From f9d3f1ac568c913b236a93757ba862e5ae72f3c4 Mon Sep 17 00:00:00 2001 From: Sergey Karmanov Date: Sat, 30 May 2026 01:16:58 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20prometheus=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Middleware/LocalNetworksOnlyMiddleware.cs | 71 +++++++++++++++++++ backend/UniVerse.Api/Program.cs | 8 +++ backend/UniVerse.Api/UniVerse.Api.csproj | 1 + 3 files changed, 80 insertions(+) create mode 100644 backend/UniVerse.Api/Middleware/LocalNetworksOnlyMiddleware.cs diff --git a/backend/UniVerse.Api/Middleware/LocalNetworksOnlyMiddleware.cs b/backend/UniVerse.Api/Middleware/LocalNetworksOnlyMiddleware.cs new file mode 100644 index 0000000..db9d6a4 --- /dev/null +++ b/backend/UniVerse.Api/Middleware/LocalNetworksOnlyMiddleware.cs @@ -0,0 +1,71 @@ +using System.Net; +using System.Net.Sockets; + +namespace UniVerse.Api.Middleware; + +public sealed class LocalNetworksOnlyMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public LocalNetworksOnlyMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + var remoteIpAddress = context.Connection.RemoteIpAddress; + + if (remoteIpAddress is null || !IsLocalNetwork(remoteIpAddress)) + { + _logger.LogWarning("Blocked metrics request from non-local address {RemoteIpAddress}", remoteIpAddress); + context.Response.StatusCode = StatusCodes.Status403Forbidden; + await context.Response.WriteAsync("Metrics endpoint is available only from local networks."); + return; + } + + await _next(context); + } + + private static bool IsLocalNetwork(IPAddress ipAddress) + { + if (IPAddress.IsLoopback(ipAddress)) + { + return true; + } + + if (ipAddress.IsIPv4MappedToIPv6) + { + ipAddress = ipAddress.MapToIPv4(); + } + + return ipAddress.AddressFamily switch + { + AddressFamily.InterNetwork => IsPrivateOrLinkLocalIPv4(ipAddress), + AddressFamily.InterNetworkV6 => IsPrivateOrLinkLocalIPv6(ipAddress), + _ => false + }; + } + + private static bool IsPrivateOrLinkLocalIPv4(IPAddress ipAddress) + { + var bytes = ipAddress.GetAddressBytes(); + + return bytes[0] == 10 + || bytes[0] == 127 + || (bytes[0] == 192 && bytes[1] == 168) + || (bytes[0] == 172 && bytes[1] is >= 16 and <= 31) + || (bytes[0] == 169 && bytes[1] == 254); + } + + private static bool IsPrivateOrLinkLocalIPv6(IPAddress ipAddress) + { + var bytes = ipAddress.GetAddressBytes(); + + return ipAddress.IsIPv6LinkLocal + || ipAddress.IsIPv6SiteLocal + || (bytes[0] & 0xfe) == 0xfc; + } +} diff --git a/backend/UniVerse.Api/Program.cs b/backend/UniVerse.Api/Program.cs index a9d76c9..e6a1089 100644 --- a/backend/UniVerse.Api/Program.cs +++ b/backend/UniVerse.Api/Program.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi; +using Prometheus; using Quartz; using Serilog; using UniVerse.Api.BackgroundServices; @@ -221,6 +222,7 @@ if (app.Environment.IsDevelopment()) app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); +app.UseHttpMetrics(); if (app.Environment.IsDevelopment()) { app.UseAntiforgery(); @@ -228,4 +230,10 @@ if (app.Environment.IsDevelopment()) } app.MapControllers(); +// Restrict Prometheus scrape endpoint to local and private networks. +app.UseWhen( + context => context.Request.Path.StartsWithSegments("/metrics", StringComparison.OrdinalIgnoreCase), + branch => branch.UseMiddleware()); +app.MapMetrics(); + app.Run(); diff --git a/backend/UniVerse.Api/UniVerse.Api.csproj b/backend/UniVerse.Api/UniVerse.Api.csproj index 1f9a164..70e2746 100644 --- a/backend/UniVerse.Api/UniVerse.Api.csproj +++ b/backend/UniVerse.Api/UniVerse.Api.csproj @@ -30,6 +30,7 @@ +