Net-Base Revista

23.05.2026

Proxy inverso con nginx y Delphi: gestión correcta del encabezado Forwarded, IP real del cliente y bases de URL robustas

Si los servidores Delphi-REST funcionan detrás de nginx, con frecuencia se alteran la IP del cliente, la detección de HTTPS y las URL absolutas. Este fragmento de código fuente muestra una gestión robusta de Forwarded/X-Forwarded (incluida la lista de proxies de confianza), ajustes típicos de nginx y notas de depuración para el funcionamiento.

23.05.2026

Un proxy inverso con nginx y Delphi en la práctica suele ser algo más que un “nice to have”: es la separación limpia entre el perímetro de Internet y la aplicación: terminación TLS (offloading de HTTPS), reglas centrales de cabeceras/CORS, límites de tasa, registros unificados, despliegues Blue/Green o simplemente el alojamiento de varios servicios bajo un dominio. Lo que a menudo se subestima es lo siguiente: en cuanto nginx se sitúa “delante”, el servidor Delphi solo ve la IP del proxy, a menudo solo “http” en lugar de “https” y genera enlaces absolutos incorrectos (redirecciones, URL de callback, URL del servidor OpenAPI). Precisamente estos tres puntos generan más tarde tiempo de depuración en producción.

Este fragmento de código muestra un patrón robusto sobre cómo en Delphi evaluar correctamente Forwarded y X-Forwarded-* —incluida una lista de proxies de confianza (importante contra el spoofing de cabeceras) y una Request-Base-URL coherente. Además incluye configuraciones de nginx aptas para la práctica y notas sobre casos límite como WebSockets, cargas grandes y tiempos de espera.

Por qué las configuraciones de proxy inverso “confunden” a los servidores Delphi

nginx se comunica como proxy inverso con el servicio Delphi típicamente sin cifrar (HTTP) en la red interna o en localhost, mientras que el cliente llega desde fuera por HTTPS. Sin cabeceras adicionales, Delphi no sabe nada de:

  • Esquema original (https vs. http) – relevante para redirecciones y URLs absolutas.
  • Host original (dominio específico del cliente, puerto) – relevante para configuraciones multi-tenant, CORS y URL de callback.
  • IP del cliente original – relevante para auditoría, límites de tasa, comprobaciones geográficas y análisis de seguridad.

nginx puede transportar esta información mediante cabeceras. Lo habitual son X-Forwarded-For, X-Forwarded-Proto y X-Forwarded-Host; además está estandarizada la cabecera RFC Forwarded. Importante: estas cabeceras no son automáticamente de confianza desde la perspectiva de la aplicación, porque un cliente puede enviarlas por sí mismo —solo son fiables cuando proceden de un proxy conocido.

Configuración de nginx: los encabezados proxy mínimamente necesarios

Un punto de partida sólido (HTTP/1.1, Keep-Alive, Upgrade para WebSockets) se ve así. El fragmento se mantiene deliberadamente conciso; deberá complementar según el entorno con HSTS, límites de tasa y registros de acceso.

Delphi
# (nginx-Konfiguration, kein Delphi)
server {
  listen 443 ssl http2;
  server_name api.example.com;

  # ssl_certificate ...; ssl_certificate_key ...;

  location / {
    proxy_http_version 1.1;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # optional, aber praktisch für absolute URLs
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header X-Forwarded-Port  $server_port;

    # WebSockets / Upgrade
    proxy_set_header Upgrade           $http_upgrade;
    proxy_set_header Connection        $connection_upgrade;

    proxy_pass http://127.0.0.1:8080;

    # Timeouts passend zu Delphi-Backend (lange Reports/Exports)
    proxy_connect_timeout 5s;
    proxy_send_timeout    60s;
    proxy_read_timeout    60s;

    # große Uploads explizit steuern
    client_max_body_size 50m;
  }
}

Propósito: La aplicación recibe de forma fiable Host, IP del cliente y esquema transmitidos. Condición: $proxy_add_x_forwarded_for añade la IP del proxy actual a una cadena existente si la hay; esto es útil para entornos con múltiples proxies, pero hace que la evaluación correcta en la página Delphi sea aún más importante. Precaución: Si en nginx no establece la cabecera Host, Delphi podría ver solo el host upstream (127.0.0.1), lo que rompe redirecciones y comprobaciones de origen.

Delphi Fragmento de código fuente: Evaluación robusta de Forwarded/X-Forwarded (con lista de proxies de confianza)

El siguiente código se mantiene deliberadamente neutral respecto al framework: opera sobre una interfaz mínima (Header + RemoteIP) y puede adaptarse a WebBroker, RAD Server o Horse. Puntos clave:

  • Prioridad: RFC Forwarded (si existe) antes que X-Forwarded-*.
  • Confianza: evaluar los encabezados Forwarded solo si el peer directo (RemoteIP) es un proxy conocido.
  • Análisis: tener en cuenta IPv6, comillas, puertos y cadenas en X-Forwarded-For.
  • Salida: una Base-URL que puede utilizar para enlaces absolutos, redirecciones u OpenAPI.

unit Net-Base.ProxyForwarding;

interface

uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;

type
// Interfaz mínima de adaptador: impleméntela para WebBroker/Horse/etc.
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // contraparte TCP directa (habitualmente nginx)
end;

TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // p. ej. https://api.example.com
end;

TTrustedProxyList = class
private
FSet: TDictionary;
class function NormalizeIp(const AIP: string): string; static;
public
constructor Create;
destructor Destroy; override;
procedure Add(const AIP: string);
function Contains(const AIP: string): Boolean;
end;

function ResolveForwardedInfo(const Req: IHeaderReader; const TrustedProxies: TTrustedProxyList): TForwardedInfo;

implementation

function StripBrackets(const S: string): string;
begin
Result := S.Trim;
if (Result.StartsWith(‚[‚)) and (Result.EndsWith(‚]‘)) then
Result := Result.Substring(1, Result.Length – 2);
end;

function RemoveQuotes(const S: string): string;
var
T: string;
begin
T := S.Trim;
if (T.Length >= 2) and (((T[1] = ‚“‚) and (T[T.Length] = ‚“‚)) or ((T[1] = ““) and (T[T.Length] = ““))) then
Result := T.Substring(1, T.Length – 2)
else
Result := T;
end;

function FirstCsvToken(const S: string): string;
var
P: Integer;
begin
// X-Forwarded-For puede ser ‚client, proxy1, proxy2‘
P := S.IndexOf(‚,‘);
if P >= 0 then
Result := S.Substring(0, P).Trim
else
Result := S.Trim;
end;

procedure SplitHostPort(const HostPort: string; out Host: string; out Port: Integer);
var
S: string;
P: Integer;
begin
Host := “;
Port := 0;
S := HostPort.Trim;

// IPv6 entre []: [2001:db8::1]:443
if S.StartsWith(‚[‚) then
begin
P := S.IndexOf(‚]‘);
if P >= 0 then
begin
Host := StripBrackets(S.Substring(0, P + 1));
if (P + 1 < S.Length) and (S[P + 2] = ':') then Port := StrToIntDef(S.Substring(P + 2), 0); Exit; end; end; // IPv4/Host: host:port (Atención: con IPv6 sin [] no es fiable) P := S.LastIndexOf(':'); if (P > 0) and (S.IndexOf(‚:‘) = P) then
begin
Host := S.Substring(0, P);
Port := StrToIntDef(S.Substring(P + 1), 0);
end
else
Host := S;
end;

function ParseForwardedHeader(const ForwardedValue: string; out ClientIP, Proto, Host: string): Boolean;
var
// Ejemplo: Forwarded: for=203.0.113.43;proto=https;host=api.example.com
Parts: TArray;
I: Integer;
KV: TArray;
K, V: string;
FirstElement: string;
begin
Result := False;
ClientIP := “;
Proto := “;
Host := “;

if ForwardedValue.Trim = “ then
Exit;

// Varios elementos están separados por comas; tomamos el primero (más cercano al cliente)
FirstElement := FirstCsvToken(ForwardedValue);
Parts := FirstElement.Split([‚;‘]);
for I := 0 to High(Parts) do
begin
KV := Parts[I].Split([‚=‘], 2);
if Length(KV) <> 2 then
Continue;
K := KV[0].Trim.ToLower;
V := RemoveQuotes(KV[1]);

if K = ‚for‘ then
begin
// for puede ser una IP o ‚unknown‘; IPv6 puede estar entre []
V := V.Trim;
if SameText(V, ‚unknown‘) then
Continue;
// puede aparecer for=1.2.3.4:5678
if V.Contains(‚:‘) and (not V.StartsWith(‚[‚)) then
V := FirstCsvToken(V); // por precaución
ClientIP := StripBrackets(V.Split([‚:‘])[0]);
end
else if K = ‚proto‘ then
Proto := V.Trim.ToLower
else if K = ‚host‘ then
Host := V.Trim;
end;

Result := (ClientIP <> “) or (Proto <> “) or (Host <> “);
end;

{ TForwardedInfo }

function TForwardedInfo.EffectiveScheme: string;
begin
if Proto <> “ then
Exit(Proto);
Result := ‚http‘;
end;

function TForwardedInfo.EffectiveHostPort: string;
begin
if Host = “ then
Exit(“);

if (Port > 0) and not ((EffectiveScheme = ‚https‘) and (Port = 443)) and not ((EffectiveScheme = ‚http‘) and (Port = 80)) then
Result := Host + ‚:‘ + Port.ToString
else
Result := Host;
end;

function TForwardedInfo.BaseUrl: string;
var
HP: string;
begin
HP := EffectiveHostPort;
if HP = “ then
Exit(“);
Result := EffectiveScheme + ‚://‘ + HP;
end;

{ TTrustedProxyList }

constructor TTrustedProxyList.Create;
begin
inherited Create;
FSet := TDictionary.Create;
end;

destructor TTrustedProxyList.Destroy;
begin
FSet.Free;
inherited;
end;

class function TTrustedProxyList.NormalizeIp(const AIP: string): string;
begin
// Para una normalización real de IPv6 se necesitaría un análisis de IP; aquí, deliberadamente pragmático.
Result := AIP.Trim;
Result := StripBrackets(Result);
end;

procedure TTrustedProxyList.Add(const AIP: string);
begin
FSet.AddOrSetValue(NormalizeIp(AIP), True);
end;

function TTrustedProxyList.Contains(const AIP: string): Boolean;
begin
Result := FSet.ContainsKey(NormalizeIp(AIP));
end;

function ResolveForwardedInfo(const Req: IHeaderReader; const TrustedProxies: TTrustedProxyList): TForwardedInfo;
var
RemoteIP: string;
Fwd, XFF, XProto, XHost, XPort: string;
ClientIP, Proto, Host: string;
Port: Integer;
begin
Result := Default(TForwardedInfo);

RemoteIP := Req.GetRemoteIP;
Result.ClientIP := RemoteIP; // Por defecto: contraparte directa

// Solo si la contraparte directa es un proxy conocido evaluamos los encabezados Forwarded.
if (TrustedProxies <> nil) and TrustedProxies.Contains(RemoteIP) then
begin
Fwd := Req.GetHeaderValue(‚Forwarded‘);
if ParseForwardedHeader(Fwd, ClientIP, Proto, Host) then
begin
if ClientIP <> “ then Result.ClientIP := ClientIP;
if Proto <> “ then Result.Proto := Proto;
if Host <> “ then
begin
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;

// X-Forwarded-* como respaldo/complemento
XFF := Req.GetHeaderValue(‚X-Forwarded-For‘);
if (Result.ClientIP = RemoteIP) and (XFF.Trim <> “) then
Result.ClientIP := StripBrackets(FirstCsvToken(XFF));

XProto := Req.GetHeaderValue(‚X-Forwarded-Proto‘);
if (Result.Proto = “) and (XProto.Trim <> “) then
Result.Proto := FirstCsvToken(XProto).ToLower;

XHost := Req.GetHeaderValue(‚X-Forwarded-Host‘);
if (Result.Host = “) and (XHost.Trim <> “) then
Result.Host := FirstCsvToken(XHost);

XPort := Req.GetHeaderValue(‚X-Forwarded-Port‘);
Port := StrToIntDef(FirstCsvToken(XPort), 0);
if (Result.Port = 0) and (Port > 0) then
Result.Port := Port;
end;

// En última instancia, tomar Host del encabezado ‚Host‘ (si no hay encabezados de proxy)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;

end.

Propósito: Obtendrá, a partir de cada request, una vista consistente de ClientIP, Proto y Host, así como una BaseUrl. Esta información puede utilizarse de forma centralizada para registro, decisiones de seguridad (p. ej. lista de direcciones IP permitidas) y generación de enlaces.

Por qué es necesaria la lista Trust-Proxy: Sin una verificación de confianza, un atacante podría alcanzar directamente su puerto Delphi (mala configuración, enrutamiento interno, VPN) y simplemente enviar X-Forwarded-For: 127.0.0.1. Con ello serían atacables los Audit-Trails, Rate-Limits o endpoints marcados como “solo permitidos internamente”. Confíe en los encabezados Forwarded solo cuando el peer directo (RemoteIP) sea un proxy que usted controle (p. ej. 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).

Precauciones: IPv6 sin corchetes no es inequívoco en la notación Host:port. En el HTTP-Host-Header IPv6 normalmente aparece entre []; sígalo. Para rangos IP complejos (CIDR) deberá ampliar la lista de confianza (p. ej. mediante un análisis IP real).

Integración en WebBroker/Horse/RAD Server: dónde se „acopla“ el código

En WebBroker (TWebRequest) los encabezados suelen venir a través de ContentFields o GetFieldByName, la Remote-IP depende del backend del servidor. En Horse (u otros frameworks HTTP) normalmente existe Req.Headers y una propiedad de Remote-IP. Lo importante es el principio: RemoteIP debe ser la contraparte TCP, no un valor cualquiera de un encabezado.

Práctica recomendada: cree al iniciar el servicio una TTrustedProxyList desde configuración (INI/ENV), p. ej. “127.0.0.1” para despliegues locales con nginx o la IP de su Load Balancer. Después llame a ResolveForwardedInfo por request y escriba los campos en su registro estructurado (JSON-Log, Syslog o Windows Event Log).

Depuración en producción: así encuentra errores en minutos en lugar de horas

Si los requests parecen „extraños“, rara vez es culpa del propio Delphi-HTTP; suele ser una combinación de encabezados de proxy, lógica de redirección y timeouts. Tres comprobaciones de depuración que valen la pena en el día a día:

  1. Volcado de encabezados (selectivo): Registre en 4xx/5xx además Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent y Request-URI. Pero solo en errores; de lo contrario el log se encarece y se vuelve difícil de manejar.
  2. Comprobar la Base-URL: Si fallan redirecciones o callback-URLs, registre ForwardedInfo.BaseUrl. Muchos errores son inmediatamente visibles (“http://127.0.0.1” en lugar de “https://api…”).
  3. Correlación de timeouts: Un 504 desde el proxy no es lo mismo que un timeout en Delphi. nginx proxy_read_timeout y los timeouts Idle/Read del lado Delphi deben coordinarse.

Casos límite: WebSockets, streaming y requests grandes

WebSockets detrás de nginx

Para WebSockets nginx necesita los encabezados Upgrade y Connection correctos. Además el backend no debe cerrar “demasiado pronto”. En el lado Delphi es relevante que su componente WebSocket (o un endpoint SSE/Streaming) pueda convivir con reverse proxies y que Heartbeats/Keep-Alives estén implementados correctamente.

Cargas grandes y errores 413

Un clásico: Delphi acepta una subida, pero nginx la bloquea antes con 413 Request Entity Too Large. Controle esto explícitamente con client_max_body_size y ajuste los límites de request en Delphi. Para soluciones de software cercanas al proceso que manejan documentos o imágenes esto no es una excepción, sino operación normal.

Terminación HTTPS y „Secure Cookies“

Si su servicio Delphi establece cookies de sesión, normalmente deben marcarse como Secure cuando hay HTTPS externo. Que su aplicación lo haga suele depender de si “sabe” que la solicitud original fue HTTPS. Aquí es exactamente donde ayuda la evaluación consistente de X-Forwarded-Proto/Forwarded.

Cuándo merece la pena el esfuerzo — y dónde puede fallar

El enfoque mostrado merece la pena siempre que el servicio Delphi ya no esté «desnudo» en la LAN, sino que forme parte de un borde productivo: varios dominios, interfaces SSO/SAML, APIs públicas, multitenencia o requisitos de auditoría más estrictos. Se complica cuando se confía ciegamente en los encabezados Forwarded o no se documentan las topologías de proxy (varias capas de Ingress, Cloud-LB más nginx más sidecar). Entonces la IP del cliente y el esquema pasan a ser rápidamente «cualquier cosa».

Una frontera clara: si necesita reglas de confianza complejas (CIDR, redes IPv6, IPs dinámicas de LB), debería ampliar la verificación de confianza (análisis real de IPs, máscaras de red) o diseñar la infraestructura de modo que solo un proxy definido pueda alcanzar el puerto Delphi (firewall/grupos de seguridad). Al final suele ser la decisión operativa más robusta.

Conclusión: Operar de forma limpia un reverse proxy con nginx y Delphi significa gestionar Forwarded correctamente

Un reverse proxy con nginx es para Delphi-REST-Server un componente estándar, pero es el tratamiento correcto de Forwarded y X-Forwarded-* lo que hace que el despliegue sea estable en explotación. El núcleo es simple: aceptar encabezados solo de proxies de confianza, derivar de forma consistente IP del cliente/esquema/host y aplicar esa base en redirecciones, logging y comprobaciones de seguridad. Con el snippet anterior dispone de una base limpia y compatible con entornos legacy que se puede integrar en WebBroker, Horse o servidores HTTP propios.

Si desea consolidar un backend Delphi existente detrás de nginx o modernizarlo hacia Delphi REST-API und REST-Server con una línea de operación clara, una revisión técnica de la cadena de proxies y del tratamiento de encabezados suele ser la palanca más rápida. Póngase en contacto con Net-Base para una breve valoración técnica.

En el ámbito profesional, el reverse proxy de Nginx y los encabezados Forwarded también desempeñan un papel importante cuando integraciones, flujos de datos y evolución deben encajar de forma ordenada.

Discutir proyecto o iniciativa de modernización con Net-Base.

Compartir entrada

Compartir esta publicación directamente

LinkedIn, X, XING, Facebook, WhatsApp y correo electrónico están disponibles de inmediato. Para Instagram preparamos el enlace y un texto breve de inmediato.

Correo electrónico

Instagram se abre en una nueva pestaña. El enlace y el texto breve se copian previamente en el portapapeles.