@@ -0,0 +1,92 @@
using System.Net ;
using System.Net.Sockets ;
namespace SfeduSchedule.Middleware ;
/// <summary>
/// Middleware ограничивает доступ к endpoint'у (сделано для /metrics) только приватными сетями.
/// Допускаются: loopback, RFC1918 (10/8, 172.16/12, 192.168/16), link-local (169.254/16, IPv6 link-local),
/// а также уникальные локальные адреса IPv6 (fc00::/7). Любой другой источник получает 403.
/// Только метод GET.
/// </summary>
public class LocalNetworksOnlyMiddleware
{
private readonly RequestDelegate _next ;
private readonly ILogger < LocalNetworksOnlyMiddleware > _logger ;
public LocalNetworksOnlyMiddleware ( RequestDelegate next , ILogger < LocalNetworksOnlyMiddleware > logger )
{
_next = next ;
_logger = logger ;
}
public async Task InvokeAsync ( HttpContext context )
{
// Разрешаем только GET
if ( ! HttpMethods . IsGet ( context . Request . Method ) )
{
_logger . LogWarning ( "Metrics method not allowed: {Method} {Path}" , context . Request . Method , context . Request . Path ) ;
context . Response . StatusCode = StatusCodes . Status405MethodNotAllowed ;
context . Response . Headers [ "Allow" ] = "GET" ;
await context . Response . WriteAsync ( "Method Not Allowed. Only GET is supported for metrics." ) ;
return ;
}
var ip = context . Connection . RemoteIpAddress ;
// Если пришёл IPv4, инкапсулированный в IPv6 (::ffff:x.y.z.w), разворачиваем в чистый IPv4.
if ( ip is { IsIPv4MappedToIPv6 : true } )
{
ip = ip . MapToIPv4 ( ) ;
}
// Проверяем принадлежность IP локальным/приватным сетям.
if ( ip is null | | ! IsLocalNetwork ( ip ) )
{
// Фиксируем X-Forwarded-For (если есть) для диагностики за обратными прокси.
var xff = context . Request . Headers . TryGetValue ( "X-Forwarded-For" , out var xffVal ) ? xffVal . ToString ( ) : null ;
_logger . LogWarning ( "Metrics access forbidden. RemoteIP={RemoteIP}, XFF={XFF}, Path={Path}" , ip ? . ToString ( ) ? ? "null" , xff , context . Request . Path ) ;
context . Response . StatusCode = StatusCodes . Status403Forbidden ;
await context . Response . WriteAsync ( "Forbidden: metrics available only from local networks" ) ;
return ;
}
// Продолжаем конвейер, если IP допустим.
await _next ( context ) ;
}
/// <summary>
/// Определяет, принадлежит ли адрес локальным / приватным диапазонам.
/// </summary>
private static bool IsLocalNetwork ( IPAddress ip )
{
// Loopback (127.0.0.0/8, ::1)
if ( IPAddress . IsLoopback ( ip ) )
return true ;
if ( ip . AddressFamily = = AddressFamily . InterNetwork )
{
var b = ip . GetAddressBytes ( ) ;
// RFC1918: 10.0.0.0/8
if ( b [ 0 ] = = 10 ) return true ;
// RFC1918: 172.16.0.0 – 172.31.255.255 (172.16/12)
if ( b [ 0 ] = = 172 & & b [ 1 ] > = 16 & & b [ 1 ] < = 31 ) return true ;
// RFC1918: 192.168.0.0/16
if ( b [ 0 ] = = 192 & & b [ 1 ] = = 168 ) return true ;
// RFC3927: link-local 169.254.0.0/16 (APIPA)
if ( b [ 0 ] = = 169 & & b [ 1 ] = = 254 ) return true ; // link-local
return false ;
}
if ( ip . AddressFamily = = AddressFamily . InterNetworkV6 )
{
// Link-local (fe80::/10)
if ( ip . IsIPv6LinkLocal ) return true ;
var b = ip . GetAddressBytes ( ) ;
// ULA (Unique Local Address) RFC4193: fc00::/7 (fc00 – fdff)
if ( b . Length > 0 & & ( b [ 0 ] = = 0xFC | | b [ 0 ] = = 0xFD ) ) return true ; // ULA
return false ;
}
return false ;
}
}