├── 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 | [](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 | 
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 | 
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 | 
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 | }
--------------------------------------------------------------------------------