├── Modules ├── .gitignore ├── Azure.Iot.Edge.Modules.iotedgeproj └── deployment.template.json ├── .gitignore ├── Architecture ├── EdgeAccess.JPG ├── SurfaceAttackAreaIoTHub.jpg └── SurfaceAttackAreaOutOfBand.jpg ├── SecureAccess ├── Dockerfile.windows-amd64.cicd ├── Dockerfile.amd64.cicd ├── Dockerfile.arm32v7.cicd ├── Device │ ├── ITcpClient.cs │ ├── SecureCopy.cs │ ├── SecureShell.cs │ ├── RemoteDesktop.cs │ ├── IStreamingDevice.cs │ ├── IDeviceClient.cs │ ├── IClientWebSocket.cs │ ├── TcpClientWrapper.cs │ ├── DeviceClientWrapper.cs │ ├── ClientWebSocketWrapper.cs │ └── StreamDevice.cs ├── Dockerfile.windows-amd64 ├── Dockerfile.amd64 ├── .gitignore ├── Dockerfile.arm32v7 ├── Module │ ├── PureDeviceHost.cs │ ├── IDeviceHost.cs │ ├── IModuleClient.cs │ ├── DeviceHost.cs │ └── ModuleClientWrapper.cs ├── Properties │ └── PublishProfiles │ │ └── FolderProfile.pubxml ├── module.json ├── Dockerfile.amd64.debug ├── Dockerfile.arm32v7.debug ├── GlobalSuppressions.cs ├── Azure.Iot.Edge.Modules.SecureAccess.csproj └── Program.cs ├── SecureAccess.Tests ├── .gitignore ├── Azure.Iot.Edge.Modules.SecureAccess.Tests.csproj └── StreamDeviceTests.cs ├── Azure.Iot.Edge.sln ├── .gitattributes ├── README.md └── azure-pipelines.yml /Modules/.gitignore: -------------------------------------------------------------------------------- 1 | # IoT Edge Modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | .env 3 | **/config/ 4 | *.user 5 | TestResults/ -------------------------------------------------------------------------------- /Architecture/EdgeAccess.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suneetnangia/IoTEdgeAccess/HEAD/Architecture/EdgeAccess.JPG -------------------------------------------------------------------------------- /Architecture/SurfaceAttackAreaIoTHub.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suneetnangia/IoTEdgeAccess/HEAD/Architecture/SurfaceAttackAreaIoTHub.jpg -------------------------------------------------------------------------------- /Architecture/SurfaceAttackAreaOutOfBand.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/suneetnangia/IoTEdgeAccess/HEAD/Architecture/SurfaceAttackAreaOutOfBand.jpg -------------------------------------------------------------------------------- /SecureAccess/Dockerfile.windows-amd64.cicd: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.1-runtime-nanoserver-1809 2 | WORKDIR /app 3 | COPY ** ./ 4 | 5 | ENTRYPOINT ["dotnet", "Azure.Iot.Edge.Modules.SecureAccess.dll"] -------------------------------------------------------------------------------- /SecureAccess/Dockerfile.amd64.cicd: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.2-runtime-stretch-slim 2 | WORKDIR /app 3 | COPY ** ./ 4 | 5 | 6 | RUN useradd -ms /bin/bash moduleuser 7 | USER moduleuser 8 | 9 | ENTRYPOINT ["dotnet", "Azure.Iot.Edge.Modules.SecureAccess.dll"] -------------------------------------------------------------------------------- /SecureAccess/Dockerfile.arm32v7.cicd: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.2-runtime-stretch-slim-arm32v7 2 | WORKDIR /app 3 | COPY ** ./ 4 | COPY qemu-arm-static /usr/bin 5 | 6 | RUN useradd -ms /bin/bash moduleuser 7 | USER moduleuser 8 | 9 | ENTRYPOINT ["dotnet", "Azure.Iot.Edge.Modules.SecureAccess.dll"] -------------------------------------------------------------------------------- /SecureAccess/Device/ITcpClient.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | 7 | public interface ITcpClient : IDisposable 8 | { 9 | Task ConnectAsync(string host, int port); 10 | 11 | Stream GetStream(); 12 | } 13 | } -------------------------------------------------------------------------------- /SecureAccess/Dockerfile.windows-amd64: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.1-sdk AS build-env 2 | WORKDIR /app 3 | 4 | COPY *.csproj ./ 5 | RUN dotnet restore 6 | 7 | COPY . ./ 8 | RUN dotnet publish -c Release -o out 9 | 10 | FROM microsoft/dotnet:2.1-runtime-nanoserver-1809 11 | WORKDIR /app 12 | COPY --from=build-env /app/out ./ 13 | ENTRYPOINT ["dotnet", "Azure.Iot.Edge.Modules.SecureAccess.dll"] -------------------------------------------------------------------------------- /SecureAccess/Dockerfile.amd64: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.2-sdk AS build-env 2 | WORKDIR /app 3 | 4 | COPY *.csproj ./ 5 | RUN dotnet restore 6 | 7 | COPY . ./ 8 | RUN dotnet publish -c Release -o out 9 | 10 | FROM microsoft/dotnet:2.2-runtime-stretch-slim 11 | WORKDIR /app 12 | COPY --from=build-env /app/out ./ 13 | 14 | RUN useradd -ms /bin/bash moduleuser 15 | USER moduleuser 16 | 17 | ENTRYPOINT ["dotnet", "Azure.Iot.Edge.Modules.SecureAccess.dll"] -------------------------------------------------------------------------------- /SecureAccess/.gitignore: -------------------------------------------------------------------------------- 1 | # .NET Core 2 | project.lock.json 3 | project.fragment.lock.json 4 | artifacts/ 5 | **/Properties/launchSettings.json 6 | 7 | *_i.c 8 | *_p.c 9 | *_i.h 10 | *.ilk 11 | *.meta 12 | *.obj 13 | *.pch 14 | *.pdb 15 | *.pgc 16 | *.pgd 17 | *.rsp 18 | *.sbr 19 | *.tlb 20 | *.tli 21 | *.tlh 22 | *.tmp 23 | *.tmp_proj 24 | *.log 25 | *.vspscc 26 | *.vssscc 27 | .builds 28 | *.pidb 29 | *.svclog 30 | *.scc 31 | .vs 32 | 33 | [Bb]in/ 34 | [Oo]bj/ -------------------------------------------------------------------------------- /SecureAccess/Dockerfile.arm32v7: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.2-sdk AS build-env 2 | WORKDIR /app 3 | 4 | COPY *.csproj ./ 5 | RUN dotnet restore 6 | 7 | COPY . ./ 8 | RUN dotnet publish -c Release -o out 9 | 10 | FROM microsoft/dotnet:2.2-runtime-stretch-slim-arm32v7 11 | WORKDIR /app 12 | COPY --from=build-env /app/out ./ 13 | 14 | RUN useradd -ms /bin/bash moduleuser 15 | USER moduleuser 16 | 17 | ENTRYPOINT ["dotnet", "Azure.Iot.Edge.Modules.SecureAccess.dll"] -------------------------------------------------------------------------------- /SecureAccess/Module/PureDeviceHost.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Module 2 | { 3 | /// 4 | /// This device host doesnt do any module level message routing, it's purely a host for virtual devices e.g. SSH. 5 | /// 6 | public class PureDeviceHost : DeviceHost 7 | { 8 | public PureDeviceHost(IModuleClient moduleClient) 9 | : base(moduleClient) 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /SecureAccess.Tests/.gitignore: -------------------------------------------------------------------------------- 1 | # .NET Core 2 | project.lock.json 3 | project.fragment.lock.json 4 | artifacts/ 5 | **/Properties/launchSettings.json 6 | 7 | *_i.c 8 | *_p.c 9 | *_i.h 10 | *.ilk 11 | *.meta 12 | *.obj 13 | *.pch 14 | *.pdb 15 | *.pgc 16 | *.pgd 17 | *.rsp 18 | *.sbr 19 | *.tlb 20 | *.tli 21 | *.tlh 22 | *.tmp 23 | *.tmp_proj 24 | *.log 25 | *.vspscc 26 | *.vssscc 27 | .builds 28 | *.pidb 29 | *.svclog 30 | *.scc 31 | .vs 32 | 33 | [Bb]in/ 34 | [Oo]bj/ -------------------------------------------------------------------------------- /SecureAccess/Device/SecureCopy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 4 | { 5 | // Implement any service specific code here e.g. buffer size, default ports etc. 6 | public class SecureCopy : StreamDevice 7 | { 8 | public SecureCopy(string hostName, int port = 22) 9 | : base(hostName, port, "SecureCopy") 10 | { 11 | Console.WriteLine($"Secure Copy virtual device initiating..."); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /SecureAccess/Device/SecureShell.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using System; 4 | 5 | // Implement any service specific code here e.g. buffer size, default ports etc. 6 | public class SecureShell : StreamDevice 7 | { 8 | public SecureShell(string hostName, int port = 22) 9 | : base(hostName, port, "SecureShell") 10 | { 11 | Console.WriteLine($"Secure Shell virtual device initiating..."); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /SecureAccess/Device/RemoteDesktop.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using System; 4 | 5 | // Implement any service specific code here e.g. buffer size, default ports etc. 6 | public class RemoteDesktop : StreamDevice 7 | { 8 | public RemoteDesktop(string hostName, int port = 3389) 9 | : base(hostName, port, "RemoteDesktop") 10 | { 11 | Console.WriteLine($"Remote Desktop virtual device initiating..."); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /SecureAccess/Device/IStreamingDevice.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | public interface IStreamingDevice 8 | { 9 | Task OpenConnectionAsync(IDeviceClient deviceClient, IClientWebSocket webSocket, ITcpClient tcpClient, CancellationTokenSource cancellationTokenSource); 10 | 11 | string StreamDeviceName { get; } 12 | string HostName { get; } 13 | int Port { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /SecureAccess/Module/IDeviceHost.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Module 2 | { 3 | using Azure.Iot.Edge.Modules.SecureAccess.Device; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | internal interface IDeviceHost : IDisposable 9 | { 10 | Task OpenConnectionAsync(); 11 | Task OpenDeviceConnectionAsync(IStreamingDevice streamingDevice, IDeviceClient deviceClient, IClientWebSocket clientWebSocket, ITcpClient tcpClient, CancellationTokenSource cts); 12 | } 13 | } -------------------------------------------------------------------------------- /SecureAccess/Module/IModuleClient.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Module 2 | { 3 | using Microsoft.Azure.Devices.Client; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | public interface IModuleClient : IDisposable 9 | { 10 | Task OpenAsync(); 11 | Task SendEventAsync(string outputName, Message message); 12 | Task SetInputMessageHandlerAsync(string inputName, MessageHandler messageHandler, object userContext, CancellationToken cancellationToken); 13 | } 14 | } -------------------------------------------------------------------------------- /SecureAccess/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | FileSystem 8 | Release 9 | Any CPU 10 | netcoreapp2.2 11 | bin\Release\netcoreapp2.2\publish\ 12 | 13 | -------------------------------------------------------------------------------- /SecureAccess/module.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema-version": "0.0.1", 3 | "description": "", 4 | "image": { 5 | "repository": "semoduleregistry.azurecr.io/azure-iot-edge-modules-secureaccess", 6 | "tag": { 7 | "version": "0.0.1", 8 | "platforms": { 9 | "amd64": "./Dockerfile.amd64", 10 | "amd64.debug": "./Dockerfile.amd64.debug", 11 | "arm32v7": "./Dockerfile.arm32v7", 12 | "arm32v7.debug": "./Dockerfile.arm32v7.debug", 13 | "windows-amd64": "./Dockerfile.windows-amd64" 14 | } 15 | }, 16 | "buildOptions": [], 17 | "contextPath": "./" 18 | }, 19 | "language": "csharp" 20 | } 21 | -------------------------------------------------------------------------------- /SecureAccess/Dockerfile.amd64.debug: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.1-runtime-stretch-slim AS base 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y --no-install-recommends unzip procps && \ 5 | rm -rf /var/lib/apt/lists/* 6 | 7 | RUN useradd -ms /bin/bash moduleuser 8 | USER moduleuser 9 | RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg 10 | 11 | FROM microsoft/dotnet:2.1-sdk AS build-env 12 | WORKDIR /app 13 | 14 | COPY *.csproj ./ 15 | RUN dotnet restore 16 | 17 | COPY . ./ 18 | RUN dotnet publish -c Debug -o out 19 | 20 | FROM base 21 | WORKDIR /app 22 | COPY --from=build-env /app/out ./ 23 | 24 | ENTRYPOINT ["dotnet", "Azure.Iot.Edge.Modules.SecureAccess.dll"] -------------------------------------------------------------------------------- /SecureAccess/Dockerfile.arm32v7.debug: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.1-runtime-stretch-slim-arm32v7 AS base 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y --no-install-recommends unzip procps && \ 5 | rm -rf /var/lib/apt/lists/* 6 | 7 | RUN useradd -ms /bin/bash moduleuser 8 | USER moduleuser 9 | RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l ~/vsdbg 10 | 11 | FROM microsoft/dotnet:2.1-sdk AS build-env 12 | WORKDIR /app 13 | 14 | COPY *.csproj ./ 15 | RUN dotnet restore 16 | 17 | COPY . ./ 18 | RUN dotnet publish -c Debug -o out 19 | 20 | FROM base 21 | WORKDIR /app 22 | COPY --from=build-env /app/out ./ 23 | 24 | ENTRYPOINT ["dotnet", "Azure.Iot.Edge.Modules.SecureAccess.dll"] -------------------------------------------------------------------------------- /SecureAccess/Device/IDeviceClient.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using Microsoft.Azure.Devices.Client; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | public interface IDeviceClient : IDisposable 9 | { 10 | Task SetMethodHandlerAsync(string methodName, MethodCallback methodHandler, object userContext); 11 | Task WaitForDeviceStreamRequestAsync(CancellationToken cancellationToken); 12 | Task AcceptDeviceStreamRequestAsync(DeviceStreamRequest request, CancellationToken cancellationToken); 13 | Task RejectDeviceStreamRequestAsync(DeviceStreamRequest request, CancellationToken cancellationToken); 14 | } 15 | } -------------------------------------------------------------------------------- /SecureAccess.Tests/Azure.Iot.Edge.Modules.SecureAccess.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /SecureAccess/Device/IClientWebSocket.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using System; 4 | using System.Net.WebSockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | public interface IClientWebSocket : IDisposable 9 | { 10 | WebSocketState State { get; } 11 | Task ConnectAsync(Uri uri, CancellationToken cancellationToken); 12 | Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken); 13 | Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken); 14 | Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken); 15 | ClientWebSocketOptions Options { get; } 16 | } 17 | } -------------------------------------------------------------------------------- /SecureAccess/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | 2 | // This file is used by Code Analysis to maintain SuppressMessage 3 | // attributes that are applied to this project. 4 | // Project-level suppressions either have no target or are given 5 | // a specific target and scoped to a namespace, type, member, etc. 6 | 7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "", Scope = "member", Target = "~M:Azure.Iot.Edge.Modules.SecureAccess.Program.Main~System.Threading.Tasks.Task")] 8 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "", Scope = "member", Target = "~M:Azure.Iot.Edge.Modules.SecureAccess.Module.PassThroughDeviceHost.PipeMessage(Microsoft.Azure.Devices.Client.Message,System.Object)~System.Threading.Tasks.Task{Microsoft.Azure.Devices.Client.MessageResponse}")] 9 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2208:Instantiate argument exceptions correctly", Justification = "", Scope = "member", Target = "~M:Azure.Iot.Edge.Modules.SecureAccess.Program.Main~System.Threading.Tasks.Task")] -------------------------------------------------------------------------------- /SecureAccess/Device/TcpClientWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Net.Sockets; 6 | using System.Threading.Tasks; 7 | 8 | public class TcpClientWrapper : ITcpClient 9 | { 10 | private bool disposed = false; 11 | private readonly TcpClient tcpClient; 12 | 13 | public TcpClientWrapper() 14 | { 15 | this.tcpClient = new TcpClient(); 16 | } 17 | 18 | public Task ConnectAsync(string host, int port) 19 | { 20 | return this.tcpClient.ConnectAsync(host, port); 21 | } 22 | 23 | public Stream GetStream() 24 | { 25 | return this.tcpClient.GetStream(); 26 | } 27 | 28 | 29 | public void Dispose() 30 | { 31 | this.Dispose(true); 32 | GC.SuppressFinalize(this); 33 | } 34 | 35 | protected virtual void Dispose(bool disposing) 36 | { 37 | if (this.disposed) 38 | return; 39 | 40 | if (disposing) 41 | this.tcpClient.Dispose(); 42 | 43 | this.disposed = true; 44 | } 45 | 46 | ~TcpClientWrapper() 47 | { 48 | this.Dispose(false); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Modules/Azure.Iot.Edge.Modules.iotedgeproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 8 | 9 | Release 10 | AnyCPU 11 | 12 | 13 | 14 | f4396cef-a271-4305-b145-276bd1057f14 15 | 16 | 17 | Linux Arm32v7 18 | Release 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /SecureAccess/Module/DeviceHost.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Module 2 | { 3 | using Azure.Iot.Edge.Modules.SecureAccess.Device; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | /// 9 | /// Base class to host a virtual device and act as a module of its own in IoT Edge environment. 10 | /// This can be built further to have host level features e.g. multiple devices, inter module messaging. 11 | /// 12 | public abstract class DeviceHost : IDeviceHost 13 | { 14 | private bool disposed = false; 15 | private IModuleClient IotHubModuleClient { get; } 16 | 17 | public DeviceHost(IModuleClient moduleClient) 18 | { 19 | this.IotHubModuleClient = moduleClient; 20 | } 21 | 22 | public async Task OpenConnectionAsync() 23 | { 24 | // Open a connection to the Edge runtime 25 | await this.IotHubModuleClient.OpenAsync().ConfigureAwait(false); 26 | } 27 | 28 | public async Task OpenDeviceConnectionAsync(IStreamingDevice streamingDevice, IDeviceClient deviceClient, IClientWebSocket webSocket, ITcpClient tcpClient, CancellationTokenSource cts) 29 | { 30 | // Run a virtual device 31 | await streamingDevice.OpenConnectionAsync(deviceClient, webSocket, tcpClient, cts).ConfigureAwait(false); 32 | } 33 | 34 | public void Dispose() 35 | { 36 | this.Dispose(true); 37 | GC.SuppressFinalize(this); 38 | } 39 | 40 | protected virtual void Dispose(bool disposing) 41 | { 42 | if (this.disposed) 43 | return; 44 | 45 | if (disposing) 46 | this.IotHubModuleClient.Dispose(); 47 | 48 | this.disposed = true; 49 | } 50 | 51 | ~DeviceHost() 52 | { 53 | this.Dispose(false); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /SecureAccess/Azure.Iot.Edge.Modules.SecureAccess.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp2.2 5 | Azure.Iot.Edge.Modules.SecureAccess 6 | Azure.Iot.Edge.Modules.SecureAccess 7 | 8 | 9 | 10 | True 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /SecureAccess/Module/ModuleClientWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Module 2 | { 3 | using Microsoft.Azure.Devices.Client; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | public class ModuleClientWrapper : IModuleClient 9 | { 10 | private bool disposed = false; 11 | private readonly ModuleClient moduleClient; 12 | 13 | public ModuleClientWrapper() 14 | { 15 | this.moduleClient = ModuleClient.CreateFromEnvironmentAsync(TransportType.Amqp_Tcp_Only).GetAwaiter().GetResult(); 16 | } 17 | 18 | public Task OpenAsync() 19 | { 20 | return this.moduleClient.OpenAsync(); 21 | } 22 | 23 | public Task SendEventAsync(string outputName, Message message) 24 | { 25 | return this.moduleClient.SendEventAsync(outputName, message); 26 | } 27 | 28 | public Task SetInputMessageHandlerAsync(string inputName, MessageHandler messageHandler, object userContext, CancellationToken cancellationToken) 29 | { 30 | return this.moduleClient.SetInputMessageHandlerAsync(inputName, messageHandler, userContext, cancellationToken); 31 | } 32 | 33 | public Task SetMethodHandlerAsync(string methodName, MethodCallback methodHandler, object userContext) 34 | { 35 | return this.moduleClient.SetMethodHandlerAsync(methodName, methodHandler, userContext); 36 | } 37 | 38 | 39 | public void Dispose() 40 | { 41 | this.Dispose(true); 42 | GC.SuppressFinalize(this); 43 | } 44 | 45 | protected virtual void Dispose(bool disposing) 46 | { 47 | if (this.disposed) 48 | return; 49 | 50 | if (disposing) 51 | this.moduleClient.Dispose(); 52 | 53 | this.disposed = true; 54 | } 55 | 56 | ~ModuleClientWrapper() 57 | { 58 | this.Dispose(false); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /SecureAccess/Device/DeviceClientWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using Microsoft.Azure.Devices.Client; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | internal class DeviceClientWrapper : IDeviceClient 9 | { 10 | private bool disposed = false; 11 | private const TransportType deviceTransportType = TransportType.Amqp; 12 | private readonly DeviceClient deviceClient; 13 | 14 | internal DeviceClientWrapper(string connectionString) 15 | { 16 | this.deviceClient = DeviceClient.CreateFromConnectionString(connectionString, deviceTransportType); 17 | } 18 | 19 | public Task WaitForDeviceStreamRequestAsync(CancellationToken cancellationToken) 20 | { 21 | return this.deviceClient.WaitForDeviceStreamRequestAsync(cancellationToken); 22 | } 23 | 24 | public Task AcceptDeviceStreamRequestAsync(DeviceStreamRequest request, CancellationToken cancellationToken) 25 | { 26 | return this.deviceClient.AcceptDeviceStreamRequestAsync(request, cancellationToken); 27 | } 28 | 29 | public Task RejectDeviceStreamRequestAsync(DeviceStreamRequest request, CancellationToken cancellationToken) 30 | { 31 | return this.deviceClient.RejectDeviceStreamRequestAsync(request, cancellationToken); 32 | } 33 | 34 | public Task SetMethodHandlerAsync(string methodName, MethodCallback methodHandler, object userContext) 35 | { 36 | return this.deviceClient.SetMethodHandlerAsync(methodName, methodHandler, userContext); 37 | } 38 | 39 | public void Dispose() 40 | { 41 | this.Dispose(true); 42 | GC.SuppressFinalize(this); 43 | } 44 | 45 | protected virtual void Dispose(bool disposing) 46 | { 47 | if (this.disposed) 48 | return; 49 | 50 | if (disposing) 51 | this.deviceClient.Dispose(); 52 | 53 | this.disposed = true; 54 | } 55 | 56 | ~DeviceClientWrapper() 57 | { 58 | this.Dispose(false); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /SecureAccess/Device/ClientWebSocketWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using System; 4 | using System.Net.WebSockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | public class ClientWebSocketWrapper : IClientWebSocket 9 | { 10 | private bool disposed = false; 11 | private readonly ClientWebSocket clientWebSocket; 12 | 13 | public ClientWebSocketWrapper() 14 | { 15 | this.clientWebSocket = new ClientWebSocket(); 16 | } 17 | 18 | public WebSocketState State => this.clientWebSocket.State; 19 | 20 | public ClientWebSocketOptions Options { get { return this.clientWebSocket.Options; } } 21 | 22 | public Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) 23 | { 24 | return this.clientWebSocket.CloseAsync(closeStatus, statusDescription, cancellationToken); 25 | } 26 | 27 | public Task ConnectAsync(Uri uri, CancellationToken cancellationToken) 28 | { 29 | return this.clientWebSocket.ConnectAsync(uri, cancellationToken); 30 | } 31 | 32 | public Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) 33 | { 34 | return this.clientWebSocket.ReceiveAsync(buffer, cancellationToken); 35 | } 36 | 37 | public Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) 38 | { 39 | return this.clientWebSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken); 40 | } 41 | 42 | public void Dispose() 43 | { 44 | this.Dispose(true); 45 | GC.SuppressFinalize(this); 46 | } 47 | 48 | protected virtual void Dispose(bool disposing) 49 | { 50 | if (this.disposed) 51 | return; 52 | 53 | if (disposing) 54 | this.clientWebSocket.Dispose(); 55 | 56 | this.disposed = true; 57 | } 58 | 59 | ~ClientWebSocketWrapper() 60 | { 61 | this.Dispose(false); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Azure.Iot.Edge.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28922.388 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{34036844-64E7-43A5-9151-4AD90E180989}") = "Azure.Iot.Edge.Modules", "Modules\Azure.Iot.Edge.Modules.iotedgeproj", "{F4396CEF-A271-4305-B145-276BD1057F14}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Iot.Edge.Modules.SecureAccess", "SecureAccess\Azure.Iot.Edge.Modules.SecureAccess.csproj", "{1C2A1A93-0408-4459-B8A7-CD5D8A7B8984}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Iot.Edge.Modules.SecureAccess.Tests", "SecureAccess.Tests\Azure.Iot.Edge.Modules.SecureAccess.Tests.csproj", "{5BF28F0D-DCAD-485B-ACBE-6AC72E27C738}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {F4396CEF-A271-4305-B145-276BD1057F14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {F4396CEF-A271-4305-B145-276BD1057F14}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {F4396CEF-A271-4305-B145-276BD1057F14}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {F4396CEF-A271-4305-B145-276BD1057F14}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {1C2A1A93-0408-4459-B8A7-CD5D8A7B8984}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {1C2A1A93-0408-4459-B8A7-CD5D8A7B8984}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {1C2A1A93-0408-4459-B8A7-CD5D8A7B8984}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {1C2A1A93-0408-4459-B8A7-CD5D8A7B8984}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {5BF28F0D-DCAD-485B-ACBE-6AC72E27C738}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {5BF28F0D-DCAD-485B-ACBE-6AC72E27C738}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {5BF28F0D-DCAD-485B-ACBE-6AC72E27C738}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {5BF28F0D-DCAD-485B-ACBE-6AC72E27C738}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {82305FCA-EC43-444B-86BA-F281CE44D9C5} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IoT Edge Secure Remote Access 2 | [![Build Status](https://dev.azure.com/suneetnangia/IotEdgeAccess/_apis/build/status/Multi-stage%20Build%20and%20Release%20Master%20Branch?branchName=master)](https://dev.azure.com/suneetnangia/IotEdgeAccess/_build/latest?definitionId=13&branchName=master) 3 | 4 | Docker Containers (IoT Edge Module) Repo- 5 | https://hub.docker.com/r/suneetnangia/azure-iot-edge-secure-access 6 | 7 | This solution allows a secure remote access to your IoT Edge by leveraging [device stream](https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-device-streams-overview) feature of IoT Hub. 8 | 9 | The custom IoT Edge module in this solution run multiple IoT devices virtually on the edge which takes advantage of the security features of device stream. Both clients and Edge/Device makes outbound connection to the streaming endpoint of IoTHub, no inbound connection is made to either client or Edge/Device. 10 | 11 | Each virtual device is an IoT device in IoT Hub which makes an outbound connection securely to Iot Hub. Data is transferred on websockets using TCP as-is without any modification. Proxy service in the diagram below runs local to the clients and it's primary function is to authenticate against IoT Hub and broker TCP connections to websocket. A sample of this service is available [here](https://github.com/Azure-Samples/azure-iot-samples-csharp). 12 | 13 | Solution is described in below- 14 | 15 | ![solution design](./Architecture/EdgeAccess.JPG) 16 | 17 | Key Features- 18 | 1. JIT (Just in Time) Access. 19 | 2. Auditing. 20 | 3. Secure Access via Device Stream. 21 | 22 | Surface Attack Area- 23 | There are two attack areas for edge devices in general- 24 | 1. External to the local infrastructure. 25 | 2. Internal to the local infrastruture. 26 | 27 | External- 28 | ![solution design](./Architecture/SurfaceAttackAreaIoTHub.jpg) 29 | It is important to realise that the surface attack area for edge device is moved to IoT Hub in this instance. IoT Hub provides built-in battle hardened features to ensure best practices like principle of least previledge are followed and surface a single control plane for your IoT solution, lowering the overall management overhead. 30 | The diagram above depicts the layers from which a user has to go through before they can access the edge device. 31 | 32 | Internal- 33 | ![solution design](./Architecture/SurfaceAttackAreaOutOfBand.jpg) 34 | Attack can equally arise from internal network/infrastucture as well, the above layers protect the edge device by implementing layer 4/7 level isolation and by not exposing any endpoint (using Device Stream feature). 35 | 36 | Why hosting virtual devices in a module? 37 | Hosting a device virtually in a module has some benefits which can be useful in edge scenarios. 38 | 39 | 1. You can host multiple virtual devices/protocols in a single module with lower resource (memory/cpu) footprint compared to hosting multiple modules one for each protocol (e.g. SSH, SCP, RDP). 40 | 2. You do not have to expose unbounded ports on the edge, each virtual device can be restricted to a specific port, mitigating the risk. 41 | 3. Each virtual device can be individually disconnected on-demand basis from IoT Hub to allow Just in Time (JIT) access. 42 | 4. Single management plane (IoT Hub) for access management. 43 | 5. Secure reverse connect mechanism underpinned by device stream feature. 44 | 45 | #### Do not use this for application level connectivity which requires low latency and high throughputs, this is designed for on-demand/occasional access to the edge devices for debug or config reasons. One such example is when you want to remove the unused docker images from the edge. 46 | 47 | To learn more about device stream feature, see here- 48 | https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-device-streams-overview 49 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | trigger: 6 | branches: 7 | include: 8 | - master 9 | paths: 10 | exclude: 11 | - README.md 12 | - azure-pipelines.yml 13 | stages: 14 | - stage: Build 15 | jobs: 16 | - job: Build 17 | pool: 18 | vmImage: 'ubuntu-latest' 19 | steps: 20 | - task: UseDotNet@2 21 | inputs: 22 | packageType: 'sdk' 23 | version: '2.2.300' 24 | - task: DotNetCoreCLI@2 25 | displayName: 'Publish Artefacts' 26 | inputs: 27 | command: 'publish' 28 | publishWebProjects: false 29 | projects: '**/Azure.Iot.Edge.Modules.SecureAccess.csproj' 30 | arguments: '-c Debug -o published' 31 | zipAfterPublish: false 32 | - task: DotNetCoreCLI@2 33 | displayName: 'Unit Tests' 34 | inputs: 35 | command: 'test' 36 | projects: '**/*Tests*.csproj' 37 | testRunTitle: 'Unit Tests' 38 | - task: PublishBuildArtifacts@1 39 | inputs: 40 | PathtoPublish: 'SecureAccess/published/SecureAccess' 41 | ArtifactName: 'Build' 42 | publishLocation: 'Container' 43 | - stage: Deploy 44 | dependsOn: 45 | - Build 46 | jobs: 47 | - job: ARM32v7_Image_Deploy 48 | pool: 49 | vmImage: 'ubuntu-latest' 50 | steps: 51 | - task: DownloadPipelineArtifact@2 52 | inputs: 53 | artifact: "Build" 54 | targetPath: "$(Build.ArtifactStagingDirectory)" 55 | - task: Bash@3 56 | inputs: 57 | targetType: 'inline' 58 | script: 'sudo apt update && sudo apt install qemu-user-static -y' 59 | - task: Bash@3 60 | inputs: 61 | targetType: 'inline' 62 | script: 'sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset' 63 | - task: Bash@3 64 | inputs: 65 | targetType: 'inline' 66 | script: 'sudo cp /usr/bin/qemu-arm-static $(Build.ArtifactStagingDirectory)' 67 | - task: Docker@2 68 | inputs: 69 | containerRegistry: 'Suneet Nangia Public Docker Hub' 70 | repository: 'suneetnangia/azure-iot-edge-secure-access' 71 | command: 'buildAndPush' 72 | Dockerfile: '**/Dockerfile.arm32v7.cicd' 73 | tags: '$(Build.BuildId)-ci-arm32v7' 74 | buildContext: '$(Build.ArtifactStagingDirectory)' 75 | - job: Linux_Image_Deploy 76 | pool: 77 | vmImage: 'ubuntu-latest' 78 | steps: 79 | - task: DownloadPipelineArtifact@2 80 | inputs: 81 | artifact: "Build" 82 | targetPath: "$(Build.ArtifactStagingDirectory)" 83 | - task: Docker@2 84 | inputs: 85 | containerRegistry: 'Suneet Nangia Public Docker Hub' 86 | repository: 'suneetnangia/azure-iot-edge-secure-access' 87 | command: 'buildAndPush' 88 | Dockerfile: './SecureAccess/Dockerfile.amd64.cicd' 89 | tags: '$(Build.BuildId)-ci-linux64' 90 | buildContext: '$(Build.ArtifactStagingDirectory)' 91 | - job: Windows_Image_Deploy 92 | pool: 93 | vmImage: 'windows-2019' 94 | steps: 95 | - task: DownloadPipelineArtifact@2 96 | inputs: 97 | artifact: "Build" 98 | targetPath: "$(Build.ArtifactStagingDirectory)" 99 | - task: Docker@2 100 | inputs: 101 | containerRegistry: 'Suneet Nangia Public Docker Hub' 102 | repository: 'suneetnangia/azure-iot-edge-secure-access' 103 | command: 'buildAndPush' 104 | Dockerfile: '**/Dockerfile.windows-amd64.cicd' 105 | tags: '$(Build.BuildId)-ci-win64' 106 | buildContext: '$(Build.ArtifactStagingDirectory)' -------------------------------------------------------------------------------- /SecureAccess/Device/StreamDevice.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Device 2 | { 3 | using Microsoft.Azure.Devices.Client; 4 | 5 | using System; 6 | using System.IO; 7 | using System.Net.WebSockets; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | public class StreamDevice : IStreamingDevice 12 | { 13 | private const int bufferSize = 1024; 14 | 15 | public StreamDevice(string hostName, int port, string deviceName) 16 | { 17 | this.HostName = hostName; 18 | this.Port = port; 19 | this.StreamDeviceName = deviceName; 20 | } 21 | 22 | /// 23 | /// Host name or IP address of the target service e.g. localhost. 24 | /// 25 | public string HostName { get; } 26 | 27 | /// 28 | /// Port number of the target service e.g. 22. 29 | /// 30 | public int Port { get; } 31 | 32 | /// 33 | /// Stream device name. 34 | /// 35 | public string StreamDeviceName { get; } 36 | 37 | public async Task OpenConnectionAsync(IDeviceClient deviceClient, IClientWebSocket clientWebSocket, ITcpClient tcpClient, CancellationTokenSource cancellationTokenSource) 38 | { 39 | DeviceStreamRequest streamRequest = await deviceClient.WaitForDeviceStreamRequestAsync(cancellationTokenSource.Token).ConfigureAwait(false); 40 | 41 | if (streamRequest != null) 42 | { 43 | await deviceClient.AcceptDeviceStreamRequestAsync(streamRequest, cancellationTokenSource.Token).ConfigureAwait(false); 44 | Console.WriteLine($"Device stream accepted from IoT Hub, at {DateTime.UtcNow}"); 45 | 46 | clientWebSocket.Options.SetRequestHeader("Authorization", $"Bearer {streamRequest.AuthorizationToken}"); 47 | 48 | await clientWebSocket.ConnectAsync(streamRequest.Url, cancellationTokenSource.Token).ConfigureAwait(false); 49 | Console.WriteLine($"Device stream connected to IoT Hub, at {DateTime.UtcNow}"); 50 | 51 | await tcpClient.ConnectAsync(this.HostName, this.Port).ConfigureAwait(false); 52 | Console.WriteLine($"Device stream connected to local endpoint, at {DateTime.UtcNow}"); 53 | 54 | using (var localStream = tcpClient.GetStream()) 55 | { 56 | await Task.WhenAny( 57 | this.HandleIncomingDataAsync(clientWebSocket, localStream, cancellationTokenSource.Token), 58 | this.HandleOutgoingDataAsync(clientWebSocket, localStream, cancellationTokenSource.Token) 59 | ).ConfigureAwait(false); 60 | 61 | localStream.Close(); 62 | Console.WriteLine($"Device stream closed to local endpoint, at {DateTime.UtcNow}"); 63 | } 64 | 65 | await clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, cancellationTokenSource.Token).ConfigureAwait(false); 66 | } 67 | else 68 | { 69 | await deviceClient.RejectDeviceStreamRequestAsync(streamRequest, cancellationTokenSource.Token).ConfigureAwait(false); 70 | } 71 | } 72 | 73 | private async Task HandleIncomingDataAsync(IClientWebSocket clientWebSocket, Stream localStream, CancellationToken cancellationToken) 74 | { 75 | var buffer = new byte[bufferSize]; 76 | 77 | while (clientWebSocket.State == WebSocketState.Open) 78 | { 79 | var receiveResult = await clientWebSocket.ReceiveAsync(buffer, cancellationToken).ConfigureAwait(false); 80 | 81 | await localStream.WriteAsync(buffer, 0, receiveResult.Count, cancellationToken).ConfigureAwait(false); 82 | } 83 | } 84 | 85 | private async Task HandleOutgoingDataAsync(IClientWebSocket clientWebSocket, Stream localStream, CancellationToken cancellationToken) 86 | { 87 | var buffer = new byte[bufferSize]; 88 | 89 | while (localStream.CanRead) 90 | { 91 | var receiveCount = await localStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); 92 | 93 | await clientWebSocket.SendAsync(new ArraySegment(buffer, 0, receiveCount), WebSocketMessageType.Binary, true, cancellationToken).ConfigureAwait(false); 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /Modules/deployment.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema-template": "1.0.1", 3 | "modulesContent": { 4 | "$edgeAgent": { 5 | "properties.desired": { 6 | "schemaVersion": "1.0", 7 | "runtime": { 8 | "type": "docker", 9 | "settings": { 10 | "minDockerVersion": "v1.25", 11 | "loggingOptions": "", 12 | "registryCredentials": { 13 | "registry1": { 14 | "username": "semoduleregistry", 15 | "password": "Gfu33Ch7K6=VrB7Y2Z5Nm5GEKKUPy9T3", 16 | "address": "semoduleregistry.azurecr.io" 17 | } 18 | } 19 | } 20 | }, 21 | "systemModules": { 22 | "edgeAgent": { 23 | "type": "docker", 24 | "settings": { 25 | "image": "mcr.microsoft.com/azureiotedge-agent:1.0", 26 | "createOptions": {} 27 | } 28 | }, 29 | "edgeHub": { 30 | "type": "docker", 31 | "status": "running", 32 | "restartPolicy": "always", 33 | "settings": { 34 | "image": "mcr.microsoft.com/azureiotedge-hub:1.0", 35 | "createOptions": { 36 | "HostConfig": { 37 | "PortBindings": { 38 | "5671/tcp": [ 39 | { 40 | "HostPort": "5671" 41 | } 42 | ], 43 | "8883/tcp": [ 44 | { 45 | "HostPort": "8883" 46 | } 47 | ], 48 | "443/tcp": [ 49 | { 50 | "HostPort": "443" 51 | } 52 | ] 53 | } 54 | } 55 | } 56 | } 57 | } 58 | }, 59 | "modules": { 60 | "SecureAccess": { 61 | "version": "1.0.0", 62 | "type": "docker", 63 | "status": "running", 64 | "restartPolicy": "always", 65 | "settings": { 66 | "image": "${MODULEDIR<..\\secureaccess>}", 67 | "createOptions": {} 68 | }, 69 | "env": { 70 | "sshDeviceConnectionString": { 71 | "value": "HostName=SEDataHubStreams.azure-devices.net;DeviceId=SSHDevice;SharedAccessKey=VP/Qcazf/KAhb8IDUKGs6erbSUf6g2KRKNieHanRZcY=" 72 | }, 73 | "sshTargetHost": { 74 | "value": "23.97.231.110" 75 | }, 76 | "sshTargetPort": { 77 | "value": "22" 78 | }, 79 | "scpDeviceConnectionString": { 80 | "value": "HostName=SEDataHubStreams.azure-devices.net;DeviceId=SCPDevice;SharedAccessKey=jbUbluaOD7Uo2CTE8tBma34jHOHPcKXUCwJ5YThMytc=" 81 | }, 82 | "scpTargetHost": { 83 | "value": "23.97.231.110" 84 | }, 85 | "scptargetPort": { 86 | "value": "22" 87 | }, 88 | "rdpDeviceConnectionString": { 89 | "value": "HostName=SEDataHubStreams.azure-devices.net;DeviceId=RDPDevice;SharedAccessKey=TCJMwxbLFP0Gl3YkVZP7XNfh+zy5M9K4onEV3gl1x1I=" 90 | }, 91 | "rdpTargetHost": { 92 | "value": "13.93.127.36" 93 | }, 94 | "rdpTargetPort": { 95 | "value": "3389" 96 | } 97 | } 98 | }, 99 | "Azure.Iot.Edge.Modules.SecureAccess": { 100 | "version": "1.0.0", 101 | "type": "docker", 102 | "status": "running", 103 | "restartPolicy": "always", 104 | "settings": { 105 | "image": "${MODULEDIR<../SecureAccess>}", 106 | "createOptions": {} 107 | } 108 | } 109 | } 110 | } 111 | }, 112 | "$edgeHub": { 113 | "properties.desired": { 114 | "schemaVersion": "1.0", 115 | "routes": { 116 | "SecureAccessToIoTHub": "FROM /messages/modules/SecureAccess/outputs/* INTO $upstream", 117 | "Azure.Iot.Edge.Modules.SecureAccessToIoTHub": "FROM /messages/modules/Azure.Iot.Edge.Modules.SecureAccess/outputs/* INTO $upstream" 118 | }, 119 | "storeAndForwardConfiguration": { 120 | "timeToLiveSecs": 7200 121 | } 122 | } 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /SecureAccess/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess 2 | { 3 | using Azure.Iot.Edge.Modules.SecureAccess.Device; 4 | using Azure.Iot.Edge.Modules.SecureAccess.Module; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | using System; 8 | using System.Linq; 9 | using System.Runtime.Loader; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | internal class Program 14 | { 15 | private static async Task Main() 16 | { 17 | // Wait until the app unloads or is cancelled by external triggers, use it for exceptional scnearios only. 18 | using (var cts = new CancellationTokenSource()) 19 | { 20 | AssemblyLoadContext.Default.Unloading += (ctx) => cts.Cancel(); 21 | Console.CancelKeyPress += (sender, cpe) => cts.Cancel(); 22 | 23 | // Bootstrap modules and virtual devices. 24 | var services = new ServiceCollection(); 25 | 26 | services.AddSingleton(isvc => 27 | new SecureShell(Environment.GetEnvironmentVariable("sshTargetHost"), GetPortFromEnvironmentVariable("sshTargetPort"))); 28 | 29 | services.AddSingleton(isvc => 30 | new SecureCopy(Environment.GetEnvironmentVariable("scpTargetHost"), GetPortFromEnvironmentVariable("scpTargetPort"))); 31 | 32 | services.AddSingleton(isvc => 33 | new RemoteDesktop(Environment.GetEnvironmentVariable("rdpTargetHost"), GetPortFromEnvironmentVariable("rdpTargetPort"))); 34 | 35 | services.AddSingleton(isvc => 36 | new PureDeviceHost(new ModuleClientWrapper())); 37 | 38 | // Dispose method of ServiceProvider will dispose all disposable objects constructed by it as well. 39 | using (var serviceProvider = services.BuildServiceProvider()) 40 | { 41 | // Get a new module. 42 | using (var module = serviceProvider.GetService()) 43 | { 44 | await module.OpenConnectionAsync().ConfigureAwait(false); 45 | 46 | // Run all tasks in parallel. 47 | Task.WaitAny( 48 | RunVirtualDevice(cts, serviceProvider, "SecureShell", Environment.GetEnvironmentVariable("sshDeviceConnectionString"), module), 49 | RunVirtualDevice(cts, serviceProvider, "SecureCopy", Environment.GetEnvironmentVariable("scpDeviceConnectionString"), module), 50 | RunVirtualDevice(cts, serviceProvider, "RemoteDesktop", Environment.GetEnvironmentVariable("rdpDeviceConnectionString"), module)); 51 | } 52 | } 53 | 54 | await WhenCancelled(cts.Token).ConfigureAwait(false); 55 | } 56 | } 57 | 58 | private static async Task RunVirtualDevice(CancellationTokenSource cts, ServiceProvider serviceProvider, string deviceName, string deviceConnectionString, IDeviceHost module) 59 | { 60 | // Keep on looking for the new streams on IoT Hub when the previous one closes or aborts. 61 | while (!cts.IsCancellationRequested) 62 | { 63 | try 64 | { 65 | // Run virtual device 66 | var device = serviceProvider.GetServices() 67 | .FirstOrDefault(sd => sd.StreamDeviceName.Equals(deviceName, StringComparison.InvariantCulture)); 68 | 69 | using (var deviceClient = new DeviceClientWrapper(deviceConnectionString)) 70 | { 71 | using (var clientWebSocket = new ClientWebSocketWrapper()) 72 | { 73 | using (var tcpClient = new TcpClientWrapper()) 74 | { 75 | Console.WriteLine($"{deviceName} awaiting connection..."); 76 | await module.OpenDeviceConnectionAsync(device, deviceClient, clientWebSocket, tcpClient, cts) 77 | .ConfigureAwait(false); 78 | } 79 | } 80 | } 81 | } 82 | catch (Exception ex) 83 | { 84 | Console.WriteLine($"Error: {ex.Message}"); 85 | } 86 | } 87 | } 88 | 89 | private static int GetPortFromEnvironmentVariable(string key) 90 | { 91 | if (!int.TryParse(Environment.GetEnvironmentVariable(key), out var port)) 92 | { 93 | throw new ArgumentOutOfRangeException(key, "Could not convert port number to integer."); 94 | } 95 | return port; 96 | } 97 | 98 | /// 99 | /// Handles cleanup operations when app is cancelled or unloads 100 | /// 101 | public static Task WhenCancelled(CancellationToken cancellationToken) 102 | { 103 | var tcs = new TaskCompletionSource(); 104 | cancellationToken.Register(s => ((TaskCompletionSource)s).SetResult(true), tcs); 105 | return tcs.Task; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /SecureAccess.Tests/StreamDeviceTests.cs: -------------------------------------------------------------------------------- 1 | namespace Azure.Iot.Edge.Modules.SecureAccess.Tests 2 | { 3 | using Azure.Iot.Edge.Modules.SecureAccess.Device; 4 | using Microsoft.Azure.Devices.Client; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | using Moq; 8 | using System; 9 | using System.IO; 10 | using System.Net.WebSockets; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | [TestClass] 15 | public class StreamDeviceTests 16 | { 17 | private static readonly Uri uri = new Uri("ws://dummy.com"); 18 | private static readonly int localPort = 22; 19 | private static readonly string localhost = "localhost"; 20 | private static readonly int streamReturnValue = 1; 21 | private static readonly int bufferSize = 1024; 22 | 23 | private Mock deviceClientMock; 24 | private Mock clientWebSocket; 25 | private Mock tcpClient; 26 | private Mock networkStream; 27 | private ClientWebSocket realClientWebSocket; 28 | private CancellationTokenSource cancellationTokenSource; 29 | 30 | private readonly byte[] buffer = new byte[bufferSize]; 31 | private bool toggleStateWebSocket = false; 32 | private bool toggleStateNetworkStream = false; 33 | private DeviceStreamRequest deviceStreamRequest; 34 | 35 | [TestInitialize] 36 | public void Setup() 37 | { 38 | this.deviceClientMock = new Mock(); 39 | this.clientWebSocket = new Mock(); 40 | this.tcpClient = new Mock(); 41 | this.networkStream = new Mock(); 42 | this.realClientWebSocket = new ClientWebSocket(); 43 | this.cancellationTokenSource = new CancellationTokenSource(); 44 | 45 | this.deviceStreamRequest = new DeviceStreamRequest("001", "teststream01", uri, "dsdfsdrer32"); 46 | 47 | this.deviceClientMock.Setup(dc => dc.WaitForDeviceStreamRequestAsync(this.cancellationTokenSource.Token)) 48 | .ReturnsAsync(() => { return this.deviceStreamRequest; }); 49 | 50 | this.clientWebSocket.Setup(dc => dc.ConnectAsync(uri, this.cancellationTokenSource.Token)) 51 | .Returns(Task.FromResult(0)); 52 | this.clientWebSocket.Setup(dc => dc.ReceiveAsync(this.buffer, this.cancellationTokenSource.Token)) 53 | .ReturnsAsync(new WebSocketReceiveResult(streamReturnValue, WebSocketMessageType.Binary, true)); 54 | 55 | this.clientWebSocket.Setup(dc => dc.Options).Returns(this.realClientWebSocket.Options); 56 | this.clientWebSocket.Setup(dc => dc.State).Returns(() => { this.toggleStateWebSocket = !this.toggleStateWebSocket; return this.toggleStateWebSocket ? WebSocketState.Open : WebSocketState.Closed; }); 57 | 58 | this.tcpClient.Setup(tc => tc.ConnectAsync(localhost, localPort)).Returns(Task.FromResult(0)); 59 | this.tcpClient.Setup(tc => tc.GetStream()).Returns(this.networkStream.Object); 60 | 61 | this.networkStream.Setup(ns => ns.WriteAsync(It.IsAny(), It.IsAny(), It.IsAny(), this.cancellationTokenSource.Token)).Returns(Task.FromResult(0)); 62 | this.networkStream.Setup(ns => ns.CanRead).Returns(() => { this.toggleStateNetworkStream = !this.toggleStateNetworkStream; return this.toggleStateNetworkStream ? true : false; }); 63 | 64 | this.networkStream.Setup(ns => ns.ReadAsync(It.IsAny(), It.IsAny(), It.IsAny(), this.cancellationTokenSource.Token)).ReturnsAsync((byte[] r, int o, int s, CancellationToken cts) => 65 | { 66 | r[0] = 1; 67 | return streamReturnValue; 68 | }); 69 | } 70 | 71 | [TestCleanup] 72 | public void CleanUp() 73 | { 74 | this.realClientWebSocket.Dispose(); 75 | this.cancellationTokenSource.Dispose(); 76 | } 77 | 78 | 79 | [TestMethod] 80 | public async Task CanAcceptDeviceStreamRequest() 81 | { 82 | // Arrange 83 | var secureShellDevice = new SecureShell(localhost, localPort); 84 | 85 | // Act 86 | await secureShellDevice.OpenConnectionAsync(this.deviceClientMock.Object, this.clientWebSocket.Object, this.tcpClient.Object, this.cancellationTokenSource); 87 | 88 | // Assert 89 | this.deviceClientMock.Verify(dc => dc.AcceptDeviceStreamRequestAsync(this.deviceStreamRequest, this.cancellationTokenSource.Token), Times.Once); 90 | } 91 | 92 | [TestMethod] 93 | public async Task CanOpenWebSocketToIoTHub() 94 | { 95 | // Arrange 96 | var secureShellDevice = new SecureShell(localhost, localPort); 97 | 98 | // Act 99 | await secureShellDevice.OpenConnectionAsync(this.deviceClientMock.Object, this.clientWebSocket.Object, this.tcpClient.Object, this.cancellationTokenSource); 100 | 101 | // Assert 102 | this.clientWebSocket.Verify(cws => cws.ConnectAsync(uri, this.cancellationTokenSource.Token), Times.Once); 103 | } 104 | 105 | [TestMethod] 106 | public async Task CanReadFromWebSocketToLocalStream() 107 | { 108 | // Arrange 109 | var secureShellDevice = new SecureShell(localhost, localPort); 110 | 111 | // Act 112 | await secureShellDevice.OpenConnectionAsync(this.deviceClientMock.Object, this.clientWebSocket.Object, this.tcpClient.Object, this.cancellationTokenSource); 113 | 114 | // Assert 115 | this.tcpClient.Verify(tc => tc.GetStream(), Times.Once); 116 | this.clientWebSocket.Verify(cws => cws.ReceiveAsync(this.buffer, this.cancellationTokenSource.Token), Times.Once); 117 | this.networkStream.Verify(ns => ns.WriteAsync(this.buffer, 0, streamReturnValue, this.cancellationTokenSource.Token), Times.Once); 118 | } 119 | 120 | [TestMethod] 121 | public async Task CanReadFromLocalStreamToWebSocket() 122 | { 123 | // Arrange 124 | var secureShellDevice = new SecureShell(localhost, localPort); 125 | 126 | // Act 127 | await secureShellDevice.OpenConnectionAsync(this.deviceClientMock.Object, this.clientWebSocket.Object, this.tcpClient.Object, this.cancellationTokenSource); 128 | 129 | // Assert 130 | this.tcpClient.Verify(tc => tc.GetStream(), Times.Once); 131 | this.networkStream.Verify(ns => ns.ReadAsync(It.IsAny(), 0, bufferSize, this.cancellationTokenSource.Token), Times.Once); 132 | this.clientWebSocket.Verify(cws => cws.SendAsync(It.IsAny>(), WebSocketMessageType.Binary, true, this.cancellationTokenSource.Token), Times.Once); 133 | } 134 | } 135 | } --------------------------------------------------------------------------------