Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ By leveraging a self-hosted architecture on Azure Container Apps, organizations
## Capabilities:

### Security
- ** Virtual Network Injection:** Secure mission-critical workloads with native **VNET Integration** and identity-based access through [Microsoft Entra ID (Managed Identity)](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) across public and sovereign regions.
- **Virtual Network Injection:** Secure mission-critical workloads with native **VNET Integration** and identity-based access through [Microsoft Entra ID (Managed Identity)](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) across public and sovereign regions.
- **Identity-Driven Edge Security:** Enforce **Zero Trust** principles with integrated **OAuth2 authentication** and customizable **Header Policy Enforcement** to validate or restrict inbound request structures.
- **Dynamic Access Governance:** Centrally manage user and group permissions via **External Configuration Providers**, enabling real-time suspension or restriction of access without code changes.

Expand Down
14 changes: 14 additions & 0 deletions ReleaseNotes/version2.2.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Release Notes #

## 2.2.9

Proxy:
* Added CheckingBackgroundRequestStatus and BackgroundRequestSubmitted as a status for background requests
* Fix bugs related to background request lifecycle
Expand Down Expand Up @@ -27,12 +29,24 @@ Proxy:
* Add streamlined probe server with ability to run as side car
* Reworked ProxyWroker to match the response flow from the 2.1.x release
* Track HTTP response in proxy data and dispose after delivery to client
* Updated to dotnet 10 and latest stable libs
* Updated Dockerimage for dotnet 10 build

Policy:
* Update backend logs for better readability
* Created a V2 policy for readability improvements
* Added section to return 408 on timeout, 429 on concurrencyLimits, and on 400 return response body

## 2.2.8.p1

Proxy:
* Added CheckingBackgroundRequestStatus and BackgroundRequestSubmitted as a status for background requests
* Fix bugs related to background request lifecycle

Policy:
* Update backend logs for better readability
* Created a V2 policy for readability improvements

## 2.2.8

Policy:
Expand Down
2 changes: 0 additions & 2 deletions src/SimpleL7Proxy/Backend/Backends.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ public class Backends : IBackendService
private static double _successRate;
private static DateTime _lastStatusDisplay = DateTime.Now - TimeSpan.FromMinutes(10); // Force display on first run
private static DateTime _lastGCTime = DateTime.Now;
private static bool _isRunning = false;
private readonly ICircuitBreaker _circuitBreaker;

private CancellationTokenSource _cancellationTokenSource;
Expand Down Expand Up @@ -308,7 +307,6 @@ private async Task<bool> GetHostStatus(BaseHostHealth host, HttpClient client)
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cancellationToken);

_probeEvent["Code"] = response.StatusCode.ToString();
_isRunning = true;

// CRITICAL: Drain the response body to allow connection reuse
// Without this, undrained responses accumulate and leak memory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,6 @@ private static BackendOptions LoadBackendOptions()
UseOAuthGov = ReadEnvironmentVariableOrDefault("UseOAuthGov", false),
UseProfiles = ReadEnvironmentVariableOrDefault("UseProfiles", false),
UserConfigUrl = ReadEnvironmentVariableOrDefault("UserConfigUrl", "file:config.json"),
UserConfigRequired = ReadEnvironmentVariableOrDefault("UserConfigRequired", false),
UserIDFieldName = ReadEnvironmentVariableOrDefault("LookupHeaderName", "UserIDFieldName", "userId"), // migrate from LookupHeaderName
UserPriorityThreshold = ReadEnvironmentVariableOrDefault("UserPriorityThreshold", 0.1f),
UserProfileHeader = ReadEnvironmentVariableOrDefault("UserProfileHeader", "X-UserProfile"),
Expand Down
1 change: 0 additions & 1 deletion src/SimpleL7Proxy/Config/BackendOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ public class BackendOptions
public bool UseProfiles { get; set; } = false;
public string UserProfileHeader { get; set; } = "";
public string UserConfigUrl { get; set; } = "";
public bool UserConfigRequired { get; set; } = false;
public float UserPriorityThreshold { get; set; }
public Dictionary<string, string> ValidateHeaders { get; set; } = [];
public bool ValidateAuthAppID { get; set; } = false;
Expand Down
2 changes: 1 addition & 1 deletion src/SimpleL7Proxy/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static class Constants
public const string RoundRobin = "roundrobin";
public const string Random = "random";
public const string Server = "simplel7proxy";
public const string VERSION = "2.3.8.dnet10.3";
public const string VERSION = "2.2.9";

public const int AnyPriority = -1;

Expand Down
32 changes: 21 additions & 11 deletions src/SimpleL7Proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,33 +1,43 @@
# Use the official image as a parent image.
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build-env
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build-env

# Set the working directory.
WORKDIR /app

# Copy csproj and restore dependencies
COPY *.csproj ./
RUN dotnet restore
# Copy the Shared project first
COPY Shared/Shared.csproj ./Shared/
# Copy SimpleL7Proxy project file
COPY SimpleL7Proxy/SimpleL7Proxy.csproj ./SimpleL7Proxy/

# Copy the rest of the working directory contents into the container at /app.
COPY . ./
# Restore dependencies for SimpleL7Proxy (which will also restore Shared)
WORKDIR /app/SimpleL7Proxy
RUN dotnet restore

# Ensure /app/out is a directory
RUN rm -f /app/out && mkdir -p /app/out
# Copy the rest of the source code
WORKDIR /app
COPY Shared/ ./Shared/
COPY SimpleL7Proxy/ ./SimpleL7Proxy/

# Build the application.
RUN dotnet publish -c Release -o out
# Build the application from SimpleL7Proxy directory
WORKDIR /app/SimpleL7Proxy
RUN dotnet publish -c Release -o /app/out

# Use ASP.NET Core runtime image
FROM mcr.microsoft.com/dotnet/runtime:9.0
FROM mcr.microsoft.com/dotnet/aspnet:10.0

# Set the working directory.
WORKDIR /app

# Copy the build output from the build image
COPY --from=build-env /app/out .

# Copy config.json to the working directory
COPY SimpleL7Proxy/config.json .
COPY SimpleL7Proxy/auth.json .

# Expose port 443 for the application.
EXPOSE 443
EXPOSE 9000

# Define the command to run your app using `dotnet`
ENTRYPOINT ["dotnet", "SimpleL7Proxy.dll", "--urls", "http://*:443"]
2 changes: 1 addition & 1 deletion src/SimpleL7Proxy/Events/ProxyEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ public void SendEvent()
this["Type"] = "S7P-" + Type.ToString();
this["MID"] = MID ?? "N/A";
this["Ver"] = Constants.VERSION;
this["Revision"] = _options.Value.Revision;
this["Revision"] = _options!.Value.Revision;
this["ContainerApp"] = _options.Value.ContainerApp;
// Send the event to Event Hub
_eventHubClient.SendData(this);
Expand Down
2 changes: 1 addition & 1 deletion src/SimpleL7Proxy/Proxy/HealthCheckService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ namespace SimpleL7Proxy.Proxy;
public class HealthCheckService
{
private readonly IBackendService _backends;
private static BackendOptions _options;
private static BackendOptions _options=null!;
private readonly IConcurrentPriQueue<RequestData>? _requestsQueue;
private readonly IUserPriorityService? _userPriority;
private readonly IEventClient? _eventClient;
Expand Down
2 changes: 1 addition & 1 deletion src/SimpleL7Proxy/Proxy/NullAsyncWorkerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ public class NullAsyncWorkerFactory: IAsyncWorkerFactory
public AsyncWorker CreateAsync(RequestData requestData, int AsyncTriggerTimeout)
{
//NOP
return null;
return null!;
}
}
17 changes: 11 additions & 6 deletions src/SimpleL7Proxy/Proxy/ProxyWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,8 @@ public async Task TaskRunnerAsync()
{
_logger.LogDebug("[Worker:{Id}] Performing cleanup for request {Guid}", _id, incomingRequest.Guid);

// if (workerState != "Cleanup")
eventData["WorkerState"] = workerState;
if (workerState != "Cleanup")
eventData["WorkerState"] = workerState;

incomingRequest.Cleanup();

Expand Down Expand Up @@ -582,9 +582,14 @@ public async Task TaskRunnerAsync()

private async Task WriteResponseAsync(RequestData request, ProxyData pr)
{
ArgumentNullException.ThrowIfNull(pr);
ArgumentNullException.ThrowIfNull(request, "Request context is null.");
ArgumentNullException.ThrowIfNull(request.Context, "Request context is null.");

var context = request.Context;

// Set the response status code
context.Response.StatusCode = (int)pr.StatusCode!;
context.Response.StatusCode = (int)pr.StatusCode;

// Copy headers to the response
//ProxyHelperUtils.CopyHeaders(request.Headers, proxyRequest, true, _options.StripRequestHeaders);
Expand Down Expand Up @@ -1582,7 +1587,7 @@ private async Task StreamResponseAsync(RequestData request, ProxyData pr)
processor.GetStats(request.EventData, proxyResponse.Headers);
}

await _lifecycleManager.HandleBackgroundRequestLifecycle(request, processor).ConfigureAwait(false);
await _lifecycleManager.HandleBackgroundRequestLifecycle(request, processor!).ConfigureAwait(false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -1615,9 +1620,9 @@ private async Task HandleBackgroundCheckResultAsync(

var pr = new ProxyData();
ProxyHelperUtils.CopyResponseHeaders(proxyResponse, pr);
if (pr.Headers != null)
if (pr.Headers != null && request.asyncWorker != null)
{
await request.asyncWorker.WriteHeaders(proxyResponse.StatusCode, pr.Headers);
await request.asyncWorker.WriteHeaders(proxyResponse.StatusCode!, pr.Headers);
}

if (request.asyncWorker != null)
Expand Down
8 changes: 4 additions & 4 deletions src/SimpleL7Proxy/SimpleL7Proxy.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.2" />
<PackageReference Include="Azure.Core" Version="1.50.0" />
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.20.1" />
<PackageReference Include="Azure.Messaging.EventHubs" Version="5.12.2" />
<PackageReference Include="Azure.Messaging.EventHubs.Processor" Version="5.12.2" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.26.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.27.0" />
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.4.0" />
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.23.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.23.0" />
Expand Down
1 change: 0 additions & 1 deletion src/SimpleL7Proxy/User/AsyncClientInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ public AsyncClientInfo(string userId, string containerName, string sbTopicName,
UserId = userId;
ContainerName = containerName;
SBTopicName = sbTopicName;
AsyncAllowed = asyncAllowed;
AsyncBlobAccessTimeoutSecs = asyncBlobAccessTimeoutSecs;
GenerateSasTokens = generateSasTokens;
}
Expand Down
1 change: 0 additions & 1 deletion src/SimpleL7Proxy/User/IUserProfileService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ public interface IUserProfileService
public bool IsAuthAppIDValid(string? authAppId);
//public bool AsyncAllowed(string UserId);
public AsyncClientInfo? GetAsyncParams(string UserId);
public bool ServiceIsReady();

}
37 changes: 5 additions & 32 deletions src/SimpleL7Proxy/User/UserProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,12 @@ public class UserProfile : BackgroundService, IUserProfileService
private List<string> suspendedUserProfiles = new List<string>();
private List<string> authAppIDs = new List<string>();

private static bool _isInitialized = false;

public UserProfile(IOptions<BackendOptions> options, ILogger<UserProfile> logger)
{
_options = options.Value;
_logger = logger;
_UserIDFieldName = _options.UserIDFieldName;
_userInformation[Constants.Server] = new AsyncClientInfo(Constants.Server, Constants.Server, Constants.Server, false, 3600);
_userInformation[Constants.Server] = new AsyncClientInfo(Constants.Server, Constants.Server, Constants.Server, 3600);

_logger.LogDebug("[INIT] UserProfile service starting");
}
Expand Down Expand Up @@ -67,49 +65,24 @@ protected override Task ExecuteAsync(CancellationToken stoppingToken)

public async Task ConfigReader(CancellationToken cancellationToken)
{

while (!cancellationToken.IsCancellationRequested)
{
DateTime startTime = DateTime.UtcNow;
const int NormalDelayMs = 3600000; // 1 hour
const int ErrorDelayMs = 3000; // 3 seconds

try
{
await ReadUserConfigAsync(_options.UserConfigUrl, ParsingMode.profileMode).ConfigureAwait(false);
await ReadUserConfigAsync(_options.SuspendedUserConfigUrl, ParsingMode.SuspendedUserMode).ConfigureAwait(false);
await ReadUserConfigAsync(_options.ValidateAuthAppIDUrl, ParsingMode.AuthAppIDMode).ConfigureAwait(false);

// Count users, initialized when at least one user profile is loaded
if (_options.UserConfigRequired && userProfiles.Count > 0 && authAppIDs.Count > 0)
{
_isInitialized = true;
}
else if (!_options.UserConfigRequired)
{
_isInitialized = true;
}
}
catch (Exception e)
{
_logger.LogError($"Error reading user config: {e.Message}");
_isInitialized = false;
// Log error
_logger.LogInformation($"Error reading user config: {e.Message}");
}

_logger.LogInformation($"[DATA] ✓ User profiles loaded - {userProfiles.Count} users found, {suspendedUserProfiles.Count} suspended users found, {authAppIDs.Count} auth app IDs found Initialized: {_isInitialized} " );

int baseDelay = _isInitialized ? NormalDelayMs : ErrorDelayMs;
int elapsedMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
int remainingDelay = Math.Max(0, baseDelay - elapsedMs);
await Task.Delay(remainingDelay, cancellationToken);
await Task.Delay(3600000, cancellationToken);
}
}

public bool ServiceIsReady()
{
return _isInitialized;
}

public async Task ReadUserConfigAsync(string config, ParsingMode mode)
{
if (string.IsNullOrEmpty(_options.UserConfigUrl))
Expand Down Expand Up @@ -368,7 +341,7 @@ public bool IsAuthAppIDValid(string? authAppId)
if (!bool.TryParse(value, out asyncEnabled) || !asyncEnabled)
{
_logger.LogWarning($"User profile: async mode not allowed for user {userId}.");
asyncEnabled = false;
return null;
}
}
else if (field == "containername")
Expand Down
2 changes: 1 addition & 1 deletion src/SimpleL7Proxy/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"S7PPriorityKey": "12345",
"Header1": "Value1",
"Header2": "Value2",
"async-config": "enabled=true, containername=user123455, topic=status-123455"
"async-config": "enabled=true, containername=user123455, topic=status-12355"
},
{
"userId": "123457",
Expand Down
10 changes: 7 additions & 3 deletions src/SimpleL7Proxy/server.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,16 @@ public async Task Run(CancellationToken cancellationToken)
if (completedTask == getContextTask)
{
var lc = await getContextTask.ConfigureAwait(false);
if (lc == null || lc.Request == null)
{
continue;
}

// if it's a probe, then bypass all the below checks and enqueue the request
if (Constants.probes.Contains(lc?.Request?.Url?.PathAndQuery))
if (Constants.probes.Contains(lc.Request.Url?.PathAndQuery))
{
// Get ProbeData from pool using modulo rotation
var probePath = lc!.Request.Url!.PathAndQuery;
var probePath = lc.Request.Url!.PathAndQuery;
var fallthrough = false;

// Fast-path for probes to avoid queue and worker latency
Expand Down Expand Up @@ -390,7 +394,7 @@ public async Task Run(CancellationToken cancellationToken)
if (doAsync && bool.TryParse(rd.Headers[_options.AsyncClientRequestHeader], out var asyncEnabled) && asyncEnabled)
{
var clientInfo = _userProfile.GetAsyncParams(rd.profileUserId);
if (clientInfo != null && clientInfo.AsyncAllowed)
if (clientInfo != null)
{
rd.runAsync = true;
rd.AsyncBlobAccessTimeoutSecs = clientInfo.AsyncBlobAccessTimeoutSecs;
Expand Down