diff --git a/Backend/Backend.csproj b/Backend/Backend.csproj
index 3f0ed32..ec20ecf 100644
--- a/Backend/Backend.csproj
+++ b/Backend/Backend.csproj
@@ -5,7 +5,6 @@
enable
enable
246972d0-2477-4915-8d25-164c075b48f3
- Linux
@@ -20,6 +19,7 @@
+
diff --git a/Backend/Dockerfile b/Backend/Dockerfile
deleted file mode 100644
index aa0682d..0000000
--- a/Backend/Dockerfile
+++ /dev/null
@@ -1,24 +0,0 @@
-#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
-
-FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
-WORKDIR /app
-EXPOSE 80
-EXPOSE 443
-
-FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
-ARG BUILD_CONFIGURATION=Release
-WORKDIR /src
-COPY ["Backend/Backend.csproj", "Backend/"]
-RUN dotnet restore "./Backend/Backend.csproj"
-COPY . .
-WORKDIR "/src/Backend"
-RUN dotnet build "./Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
-
-FROM build AS publish
-ARG BUILD_CONFIGURATION=Release
-RUN dotnet publish "./Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
-
-FROM base AS final
-WORKDIR /app
-COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "Backend.dll"]
\ No newline at end of file
diff --git a/Backend/Program.cs b/Backend/Program.cs
index 2002ebb..9a16c70 100644
--- a/Backend/Program.cs
+++ b/Backend/Program.cs
@@ -10,6 +10,8 @@
// Backend builds a web app for management use. Optional.
var builder = WebApplication.CreateBuilder(args);
+builder.AddServiceDefaults();
+
builder.Services.AddSignalR()
.AddMessagePackProtocol(options =>
{
@@ -22,9 +24,12 @@
.AddHostedService()
.AddHttpClient();
-
var app = builder.Build();
+app.MapDefaultEndpoints();
+
+// Dummy endpoint for backend gateway. Normally the gatewat us forwarding to a destination service but
+// it might also have its own endpoints for management etc.
app.MapGet("/", () => "This is the gateway backend");
app.Run();
diff --git a/Backend/Properties/launchSettings.json b/Backend/Properties/launchSettings.json
index 249ce40..a66637f 100644
--- a/Backend/Properties/launchSettings.json
+++ b/Backend/Properties/launchSettings.json
@@ -1,38 +1,20 @@
{
"profiles": {
- "Backend": {
+ "http": {
"commandName": "Project",
- "launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
- "applicationUrl": "https://localhost:7208;http://localhost:5297"
+ "applicationUrl": "http://localhost:5297"
},
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
+ "https": {
+ "commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "Container (Dockerfile)": {
- "commandName": "Docker",
- "launchBrowser": true,
- "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
- "environmentVariables": {
- "ASPNETCORE_URLS": "https://+:443;http://+:80"
},
- "publishAllPorts": true,
- "useSSL": true
- }
- },
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true,
- "iisExpress": {
- "applicationUrl": "http://localhost:36258",
- "sslPort": 44344
+ "dotnetRunMessages": true,
+ "applicationUrl": "https://localhost:7208;http://localhost:5297"
}
}
}
\ No newline at end of file
diff --git a/Frontend/Dockerfile b/Frontend/Dockerfile
deleted file mode 100644
index 1494cbe..0000000
--- a/Frontend/Dockerfile
+++ /dev/null
@@ -1,24 +0,0 @@
-#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
-
-FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
-WORKDIR /app
-EXPOSE 80
-EXPOSE 443
-
-FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
-ARG BUILD_CONFIGURATION=Release
-WORKDIR /src
-COPY ["Frontend/Frontend.csproj", "Frontend/"]
-RUN dotnet restore "./Frontend/Frontend.csproj"
-COPY . .
-WORKDIR "/src/Frontend"
-RUN dotnet build "./Frontend.csproj" -c $BUILD_CONFIGURATION -o /app/build
-
-FROM build AS publish
-ARG BUILD_CONFIGURATION=Release
-RUN dotnet publish "./Frontend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
-
-FROM base AS final
-WORKDIR /app
-COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "Frontend.dll"]
\ No newline at end of file
diff --git a/Frontend/Frontend.csproj b/Frontend/Frontend.csproj
index 7de8eb7..9c26a1f 100644
--- a/Frontend/Frontend.csproj
+++ b/Frontend/Frontend.csproj
@@ -5,7 +5,6 @@
enable
enable
e498ca58-c3d8-46f1-b349-6db90480b0c6
- Linux
@@ -18,6 +17,7 @@
+
diff --git a/Frontend/Hubs/TunnelHub.cs b/Frontend/Hubs/TunnelHub.cs
index e503820..bded14c 100644
--- a/Frontend/Hubs/TunnelHub.cs
+++ b/Frontend/Hubs/TunnelHub.cs
@@ -14,10 +14,16 @@ public class TunnelHub : Hub
{
private readonly ConcurrentDictionary _connections = new();
- public async Task SendHttpRequestAsync(RequestMessage request)
+ public async Task SendHttpRequestAsync(RequestMessage request)
{
// TODO: target/routing. Currently just sends to first client
+ if (_connections.IsEmpty)
+ {
+ Log.Warning("No clients connected to handle request");
+ return null;
+ }
+
string connectionId = _connections.FirstOrDefault().Value;
Log.Debug("Sending to: {ConnectionId}", connectionId);
return await Clients.Client(_connections.FirstOrDefault().Value).HttpRequest(request);
diff --git a/Frontend/Middleware/TunnelMiddleware.cs b/Frontend/Middleware/TunnelMiddleware.cs
index 3696049..548b403 100644
--- a/Frontend/Middleware/TunnelMiddleware.cs
+++ b/Frontend/Middleware/TunnelMiddleware.cs
@@ -14,13 +14,22 @@ public class TunnelMiddleware(RequestDelegate nextMiddleware, TunnelHub tunnelHu
public async Task Invoke(HttpContext context)
{
- if (!context.Request.Path.StartsWithSegments("/gw-hub"))
+ var path = context.Request.Path;
+
+ if (!path.StartsWithSegments("/gw-hub") &&
+ !path.StartsWithSegments("/health") &&
+ !path.StartsWithSegments("/alive"))
{
var tunnelRequestMessage = await CreateTunnelMessage(context);
Log.Debug("Sending request {@Message}", tunnelRequestMessage);
var responseMessage = await _tunnelHub.SendHttpRequestAsync(tunnelRequestMessage);
+ if (responseMessage == null)
+ {
+ context.Response.StatusCode = StatusCodes.Status502BadGateway;
+ return;
+ }
context.Response.StatusCode = (int)responseMessage.StatusCode;
CopyFromResponseHeaders(context, responseMessage);
diff --git a/Frontend/Program.cs b/Frontend/Program.cs
index f4c8248..227c516 100644
--- a/Frontend/Program.cs
+++ b/Frontend/Program.cs
@@ -9,6 +9,8 @@
.CreateLogger();
var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
builder.Services
.AddCors()
.AddSingleton();
@@ -27,7 +29,19 @@
var app = builder.Build();
-// app.UseHttpsRedirection();
+if (!app.Environment.IsDevelopment())
+{
+ app.UseExceptionHandler("/Error", createScopeForErrors: true);
+ // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
+ app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+
+// app.UseAntiforgery();
+
+app.MapDefaultEndpoints();
+
app.UseCors(builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
diff --git a/Frontend/Properties/launchSettings.json b/Frontend/Properties/launchSettings.json
index b773a61..7e30a86 100644
--- a/Frontend/Properties/launchSettings.json
+++ b/Frontend/Properties/launchSettings.json
@@ -1,38 +1,20 @@
{
"profiles": {
- "Frontend": {
+ "http": {
"commandName": "Project",
- "launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
- "applicationUrl": "https://localhost:7175;http://localhost:5105"
+ "applicationUrl": "http://localhost:5105"
},
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
+ "https": {
+ "commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
- "Container (Dockerfile)": {
- "commandName": "Docker",
- "launchBrowser": true,
- "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
- "environmentVariables": {
- "ASPNETCORE_URLS": "https://+:443;http://+:80"
},
- "publishAllPorts": true,
- "useSSL": true
- }
- },
- "iisSettings": {
- "windowsAuthentication": false,
- "anonymousAuthentication": true,
- "iisExpress": {
- "applicationUrl": "http://localhost:59540",
- "sslPort": 44357
+ "dotnetRunMessages": true,
+ "applicationUrl": "https://localhost:7175;http://localhost:5105"
}
}
}
\ No newline at end of file
diff --git a/README.md b/README.md
index b269b86..bc16c5f 100644
--- a/README.md
+++ b/README.md
@@ -1,26 +1,41 @@
# SignalRGatewayTunnel
-Sample SignalR based API Gateway tunnel
+[](https://github.com/tjmoore/SignalRGatewayTunnel/actions/workflows/build.yml)
-WORK IN PROGRESS
+Sample SignalR based Gateway tunnel
+**WORK IN PROGRESS**
-This is a rough proof of concept of an API Gateway tunnel using SignalR. This shouldn't be relied upon as production ready, nor expect to be stable or secure.
+This is a rough proof of concept of a Gateway tunnel using SignalR. **This shouldn't be relied upon as production ready, nor expect to be stable or secure**
-The use case is for applications that reside in an on-premise or locked-down environment where it is not possible to open ports to expose the application endpoints. An application outside of that environment may need to connect to the application inside.
+The structure is as follows:
-Here a Backend service would run inside the locked environment, and a Frontend service runs externally and exposes a SignalR endpoint to listen on. The Backend connects to the Frontend and registers itself.
+- External Client (browser or other HTTP client)
+- Frontend Proxy (cloud hosted for example)
+- Backend Gateway (inside a restricted network for example)
+- Destination Backend Service (the destination service to be accessed)
+
+The Frontend Proxy hosts a SignalR Hub that the Backend Gateway connects to as a client. The Backend Gateway registers itself with the Frontend Proxy, and listens for incoming requests coming back on the SignalR connection.
+When a request is received by the Frontend Proxy, it packages the request into a message and sends it to the Backend Gateway via SignalR. The Backend Gateway then unpacks the message, makes the HTTP request to the Destination Backend Service, and sends the response back to the Frontend Proxy, which then returns it to the original client.
+
+A use case may be for example, a corporate network that restricts inbound traffic, but allows outbound HTTPS traffic to the internet. They may not wish to open firewalls and provide routing to the internal service.
+In this case, a Backend Gateway service runs inside the corporate network can connect out to a Frontend service hosted in the cloud, which then allows external clients to access the internal service via the Frontend Proxy.
+
+Likewise useful for home environments for a service hosted in a NAS for example, that needs to be accessed externally without opening up home network firewalls.
+
+This isn't intended as a backdoor or way to bypass security, but rather a framework to allow controlled access to internal services without opening up firewalls or exposing services directly to the internet.
+
+Noting the security implications of exposing internal services externally, this should be done with care, and appropriate security measures in place.
+
+**This example does not include any authentication or encryption other than what comes out of the box (HTTPS support for example)**
-The Frontend accepts HTTP requests, packages these into messages and sends to the SignalR client (the Backend), which then unpacks and sends an HTTP request inside the locked environment to an internal endpoint. The HTTP response is packaged and returned to the Backend service.
While this kind of set up is also achievable with VPNs, SSH tunnels and similar, this provides a framework for a custom tunnel or reverse proxy that could be made to handle many client applications. For example a single endpoint externally that an application or users can access, with routing to multiple backends depending on some identifier in the request, DNS, etc.
-This doesn't handle tunneling SignalR itself at present.
+Tunnelling SignalR within the SignalR connection is not support/untested. Although potentially long-polling SignalR (HTTP) requests might work, but again not tested.
The example here currently only routes to the first client that registers.
-Dockerfile configs are just defaults generated for the projects and untested.
-
## References
@@ -30,6 +45,26 @@ Loosely based Frontend middleware on https://auth0.com/blog/building-a-reverse-p
## Dependencies
-Projects are targetting .NET 8, but minimum .NET 7 for SignalR Client Results support.
+.NET 8 minimum
+
+MessagePack for fast binary package transport https://github.com/MessagePack-CSharp/MessagePack-CSharp
+
+## Running development mode
+
+#### Visual Studio
+Set SignalRGatewayTunnel.AppHost as start up project and run (F5)
+
+#### Visual Studio Code
+From Solution Explorer, right click SignalRGatewayTunnel.AppHost and select Debug -> Start New Instance
+
+#### Command Line
+Run `dotnet run --project SignalRGatewayTunnel.AppHost`
+
+You may have to select the dashboard link shown in the console output to launch in the browser
+
+This will run the .NET Aspire host, launching the components and dashboard in the browser showing the service status.
+
+If the Backend Gateway has connected to the frontend successfully, browse to the Frontend Proxy URL (http://localhost:5105 or https://localhost:7175).
+This will send the HTTP request in the browser via the gateway to the destination endpoint at http://localhost:9000 and return the response or timeout if destination is not running.
+
-MessagePack for fast binary package transport https://github.com/MessagePack-CSharp/MessagePack-CSharp
\ No newline at end of file
diff --git a/SignalRGatewayTunnel.AppHost/AppHost.cs b/SignalRGatewayTunnel.AppHost/AppHost.cs
new file mode 100644
index 0000000..028685f
--- /dev/null
+++ b/SignalRGatewayTunnel.AppHost/AppHost.cs
@@ -0,0 +1,12 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var frontend = builder.AddProject("frontend")
+ .WithExternalHttpEndpoints()
+ .WithHttpHealthCheck("/health");
+
+builder.AddProject("backend")
+ .WithHttpHealthCheck("/health")
+ .WithReference(frontend)
+ .WaitFor(frontend);
+
+builder.Build().Run();
diff --git a/SignalRGatewayTunnel.AppHost/Properties/launchSettings.json b/SignalRGatewayTunnel.AppHost/Properties/launchSettings.json
new file mode 100644
index 0000000..3413fe8
--- /dev/null
+++ b/SignalRGatewayTunnel.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17046;http://localhost:15057",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21005",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22193"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15057",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19054",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023"
+ }
+ }
+ }
+}
diff --git a/SignalRGatewayTunnel.AppHost/SignalRGatewayTunnel.AppHost.csproj b/SignalRGatewayTunnel.AppHost/SignalRGatewayTunnel.AppHost.csproj
new file mode 100644
index 0000000..896d7f0
--- /dev/null
+++ b/SignalRGatewayTunnel.AppHost/SignalRGatewayTunnel.AppHost.csproj
@@ -0,0 +1,22 @@
+
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ 2f5cf0e6-04ba-425c-9e34-c6da020ecdfb
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SignalRGatewayTunnel.AppHost/appsettings.Development.json b/SignalRGatewayTunnel.AppHost/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/SignalRGatewayTunnel.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/SignalRGatewayTunnel.AppHost/appsettings.json b/SignalRGatewayTunnel.AppHost/appsettings.json
new file mode 100644
index 0000000..31c092a
--- /dev/null
+++ b/SignalRGatewayTunnel.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/SignalRGatewayTunnel.ServiceDefaults/Extensions.cs b/SignalRGatewayTunnel.ServiceDefaults/Extensions.cs
new file mode 100644
index 0000000..b72c875
--- /dev/null
+++ b/SignalRGatewayTunnel.ServiceDefaults/Extensions.cs
@@ -0,0 +1,127 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ServiceDiscovery;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace Microsoft.Extensions.Hosting;
+
+// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
+// This project should be referenced by each service project in your solution.
+// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
+public static class Extensions
+{
+ private const string HealthEndpointPath = "/health";
+ private const string AlivenessEndpointPath = "/alive";
+
+ public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.ConfigureOpenTelemetry();
+
+ builder.AddDefaultHealthChecks();
+
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ // Turn on resilience by default
+ http.AddStandardResilienceHandler();
+
+ // Turn on service discovery by default
+ http.AddServiceDiscovery();
+ });
+
+ // Uncomment the following to restrict the allowed schemes for service discovery.
+ // builder.Services.Configure(options =>
+ // {
+ // options.AllowedSchemes = ["https"];
+ // });
+
+ return builder;
+ }
+
+ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddSource(builder.Environment.ApplicationName)
+ .AddAspNetCoreInstrumentation(tracing =>
+ // Exclude health check requests from tracing
+ tracing.Filter = context =>
+ !context.Request.Path.StartsWithSegments(HealthEndpointPath)
+ && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
+ )
+ // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
+ //.AddGrpcClientInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
+ //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
+ //{
+ // builder.Services.AddOpenTelemetry()
+ // .UseAzureMonitor();
+ //}
+
+ return builder;
+ }
+
+ public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.Services.AddHealthChecks()
+ // Add a default liveness check to ensure app is responsive
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ // Adding health checks endpoints to applications in non-development environments has security implications.
+ // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
+ if (app.Environment.IsDevelopment())
+ {
+ // All health checks must pass for app to be considered ready to accept traffic after starting
+ app.MapHealthChecks(HealthEndpointPath);
+
+ // Only health checks tagged with the "live" tag must pass for app to be considered alive
+ app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live")
+ });
+ }
+
+ return app;
+ }
+}
diff --git a/SignalRGatewayTunnel.ServiceDefaults/SignalRGatewayTunnel.ServiceDefaults.csproj b/SignalRGatewayTunnel.ServiceDefaults/SignalRGatewayTunnel.ServiceDefaults.csproj
new file mode 100644
index 0000000..45cf7fb
--- /dev/null
+++ b/SignalRGatewayTunnel.ServiceDefaults/SignalRGatewayTunnel.ServiceDefaults.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SignalRGatewayTunnel.sln b/SignalRGatewayTunnel.sln
index 49bf2e5..3849e0f 100644
--- a/SignalRGatewayTunnel.sln
+++ b/SignalRGatewayTunnel.sln
@@ -15,6 +15,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Model", "Model\Model.csproj", "{30991CA8-1085-461D-A053-D2696AB01C11}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignalRGatewayTunnel.AppHost", "SignalRGatewayTunnel.AppHost\SignalRGatewayTunnel.AppHost.csproj", "{2E96650E-0FF9-44A0-9AF3-43EB9E975A50}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignalRGatewayTunnel.ServiceDefaults", "SignalRGatewayTunnel.ServiceDefaults\SignalRGatewayTunnel.ServiceDefaults.csproj", "{9C56300F-0625-D4A8-977B-A6EC6620CB70}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -33,6 +37,14 @@ Global
{30991CA8-1085-461D-A053-D2696AB01C11}.Debug|Any CPU.Build.0 = Debug|Any CPU
{30991CA8-1085-461D-A053-D2696AB01C11}.Release|Any CPU.ActiveCfg = Release|Any CPU
{30991CA8-1085-461D-A053-D2696AB01C11}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2E96650E-0FF9-44A0-9AF3-43EB9E975A50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2E96650E-0FF9-44A0-9AF3-43EB9E975A50}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2E96650E-0FF9-44A0-9AF3-43EB9E975A50}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2E96650E-0FF9-44A0-9AF3-43EB9E975A50}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9C56300F-0625-D4A8-977B-A6EC6620CB70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9C56300F-0625-D4A8-977B-A6EC6620CB70}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9C56300F-0625-D4A8-977B-A6EC6620CB70}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9C56300F-0625-D4A8-977B-A6EC6620CB70}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE