Net-Base Magasin

23.05.2026

Reverseproxy med nginx och Delphi: korrekt hantering av Forwarded, verklig klient‑IP och robusta URL‑baser

Om Delphi-REST-servrar körs bakom nginx blir ofta klient-IP, HTTPS-identifiering och absoluta URL:er felaktiga. Denna kodsnutt visar robust hantering av Forwarded-/X-Forwarded (inkl. en trust-proxy-lista), typiska nginx-inställningar och felsökningsanvisningar för drift.

23.05.2026

En Reverse Proxy med nginx och Delphi är i praktiken oftast inte ett „nice to have“, utan den rena avgränsningen mellan internetkanten och applikationen: TLS-terminering (HTTPS-offloading), centrala header-/CORS-regler, ratebegränsningar, enhetliga loggar, Blue/Green-utrullningar eller helt enkelt hosting av flera tjänster under en domän. Vad som ofta underskattas är: så fort nginx sitter „framför“ ser Delphi-servern endast proxy-IP:n, ofta endast „http“ istället för „https“ och genererar felaktiga absoluta länkar (omdirigeringar, callback-URL:er, OpenAPI-server-URL). Just dessa tre punkter leder senare till tid för felsökning i drift.

Denna kodsnutt visar ett robust mönster för hur du i Delphi kan utvärdera Forwarded respektive X-Forwarded-* korrekt – inklusive Trust-Proxy-lista (viktig mot header-spoofing) och en konsekvent Request-Base-URL. Dessutom finns praktiska nginx-konfigurationer och anmärkningar om randfall som WebSockets, stora uppladdningar och timeouts.

Varför reverse proxy-uppsättningar Delphi-servrar „förvirrar“

nginx kommunicerar som reverse proxy med Delphi-tjänsten typiskt okrypterat (HTTP) i det interna nätverket eller på localhost, medan klienten utifrån använder HTTPS. Utan ytterligare headers vet Delphi inget om:

  • Ursprungligt schema (https vs. http) – relevant för omdirigeringar och absoluta URL:er.
  • Ursprunglig host (kundspecifik domän, port) – relevant för multi-tenant-uppsättningar, CORS och callback-URL:er.
  • Ursprunglig klient-IP – relevant för revision, ratebegränsningar, geo-kontroller och säkerhetsanalyser.

nginx kan transportera denna information via headers. Vanliga är X-Forwarded-For, X-Forwarded-Proto och X-Forwarded-Host; standardiserad finns dessutom RFC-headern Forwarded. Viktigt: Dessa headers är från applikationens perspektiv inte automatiskt betrodda, eftersom en klient själv kan skicka dem – de blir först betrodda när de kommer från en känd proxy.

nginx-konfiguration: die minimal sinnvollen Proxy-Header

En stabil startpunkt (HTTP/1.1, Keep-Alive, Upgrade för WebSockets) ser ut så här. Snippetet är avsiktligt kort; beroende på miljö kompletterar du med HSTS, ratebegränsningar och access-loggar.

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;
  }
}

Syfte: Applikationen får Host, klient‑IP och URL‑protokoll vidarebefordrade på ett tillförlitligt sätt. Förutsättning: $proxy_add_x_forwarded_for lägger till den aktuella proxy‑IP:n i en eventuell befintlig kedja; det är bra för uppsättningar med flera proxys, men gör korrekt utvärdering på Delphi-sidan ännu viktigare. Fallgrop: Om du i nginx inte anger Host-headern, ser Delphi möjligen endast upstream‑hosten (127.0.0.1), vilket bryter omdirigeringar och origin‑kontroller.

Delphi Källkodssnutt: Robust utvärdering av Forwarded/X-Forwarded (med lista över betrodda proxys)

Följande kod är medvetet ramverksneutral: den arbetar mot ett minimalt gränssnitt (Header + RemoteIP) och kan anpassas till WebBroker, RAD Server eller Horse. Kärnpunkter:

  • Prioritet: RFC Forwarded (om tillgänglig) före X-Forwarded-*.
  • Trust: Utvärdera endast Forwarded‑headern när den direkta peer:n (RemoteIP) är en känd proxy.
  • Parsing: Ta hänsyn till IPv6, citattecken, portar och kedjor i X-Forwarded-For.
  • Output: en bas‑URL som du kan använda för absoluta länkar, omdirigeringar eller OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

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

type
  // Minimales Adapter-Interface: implementieren Sie es für WebBroker/Horse/etc.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // direkte TCP-Gegenstelle (meist nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // z.B. 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 kann "client, proxy1, proxy2" sein
  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 in []: [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 (Achtung: bei nacktem IPv6 ohne [] nicht zuverlässig)
  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
  // Beispiel: Forwarded: for=203.0.113.43;proto=https;host=api.example.com
  Parts: TArray<string>;
  I: Integer;
  KV: TArray<string>;
  K, V: string;
  FirstElement: string;
begin
  Result := False;
  ClientIP := '';
  Proto := '';
  Host := '';

  if ForwardedValue.Trim = '' then
    Exit;

  // Mehrere Elemente sind per Komma getrennt; wir nehmen das erste (client-naheste)
  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 kann IP oder "unknown" sein; IPv6 kann in [] stehen
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 kommt vor
      if V.Contains(':') and (not V.StartsWith('[')) then
        V := FirstCsvToken(V); // defensiv
      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<string, Boolean>.Create;
end;

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

class function TTrustedProxyList.NormalizeIp(const AIP: string): string;
begin
  // Für echte IPv6-Normalisierung bräuchte man ein IP-Parsing; hier bewusst pragmatisch.
  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; // Fallback: direkte Gegenstelle

  // Nur wenn die direkte Gegenstelle ein bekannter Proxy ist, werten wir Forwarded-Header aus.
  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-* als Fallback/Ergänzung
    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;

  // Notfalls Host aus "Host" Header nehmen (wenn kein Proxy-Header da ist)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Syfte: Du får från varje Request en konsekvent vy av ClientIP, Proto och Host samt en BaseUrl. Denna information kan du använda centralt för loggning, säkerhetsbeslut (t.ex. IP-allowlist) och länkgenerering.

Varför Trust-Proxy-listan behövs: Utan trust‑kontroll skulle en angripare kunna nå din Delphi-port direkt (felkonfiguration, internt routing, VPN) och skicka t.ex. X-Forwarded-For: 127.0.0.1. Då blir auditloggar, ratebegränsningar eller „endpoints endast för internt bruk“ angripbara. Lita endast på Forwarded‑headers när den direkta peer‑adressen (RemoteIP) är en proxy som du kontrollerar (t.ex. 127.0.0.1, din Load-Balancer‑IP, Kubernetes‑Ingress).

Fallgropar: IPv6 utan hakparenteser är i Host:port‑notation inte entydigt. I HTTP Host‑header anges IPv6 normalt inom []; följ den konventionen. För komplexa IP‑intervall (CIDR) måste du utöka trust‑listan (t.ex. genom verklig IP‑parsing).

Integration i WebBroker/Horse/RAD Server: var koden „ansluter“

I WebBroker (TWebRequest) kommer headers vanligtvis via ContentFields eller GetFieldByName, Remote‑IP beror på server‑backend. I Horse (eller andra HTTP‑ramverk) finns oftast Req.Headers och en Remote‑IP‑egenskap. Det viktiga är principen: RemoteIP måste vara TCP‑motparten, inte något värde i en header.

Praktiskt brukbart: skapa vid service‑start en TTrustedProxyList från konfiguration (INI/ENV), t.ex. „127.0.0.1“ för lokala nginx‑upplägg eller IPn för din load balancer. Kör sedan ResolveForwardedInfo per Request och skriv fälten till ditt strukturerade logging (JSON‑Log, Syslog eller Windows Event Log).

Felsökning i drift: så hittar du fel på minuter istället för timmar

När Requests verkar „konstiga“ beror det sällan på Delphi‑HTTP i sig, utan på en kombination av proxy‑headers, redirect‑logik och timeouts. Tre felsökningskontroller som ofta lönar sig i vardagen:

  1. Header‑dump (selektiv): Logga vid 4xx/5xx dessutom Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent och Request-URI. Men bara vid fel — annars blir loggen dyr och svåröverskådlig.
  2. Kontrollera Base‑URL: När redirects eller callback‑URLs misslyckas, logga ForwardedInfo.BaseUrl. Många fel syns direkt („http://127.0.0.1“ istället för „https://api…“).
  3. Timeout‑korrelation: En 504 från proxyn är inte samma sak som ett Delphi‑timeout. nginx proxy_read_timeout och Delphi‑sida Idle/Read‑timeouts måste vara i samklang.

Särskilda fall: WebSockets, streaming och stora förfrågningar

WebSockets bakom nginx

För WebSockets krävs att nginx har Upgrade och Connection korrekt. Dessutom får backend inte stänga „för tidigt“. På Delphi‑sidan är det viktigt att din WebSocket‑komponent (eller en SSE/streaming‑endpoint) kan hantera reverse proxies och att heartbeats/keep‑alives är korrekt implementerade.

Stora uppladdningar och 413‑fel

En klassiker: Delphi accepterar en upload, men nginx blockerar tidigare med 413 Request Entity Too Large. Styr detta explicit med client_max_body_size och anpassa Delphi‑sida request‑limits. För processnära mjukvarulösningar med dokument‑ eller bilddata är detta inte en specialfall utan normal drift.

HTTPS‑avlastning och „Secure Cookies“

Om er Delphi-service sätter session-cookies måste dessa vid extern HTTPS normalt markeras som Secure. Om er applikation gör det beror ofta på om den „vet“ att den ursprungliga begäran var HTTPS. Just här hjälper en konsekvent utvärdering av X-Forwarded-Proto/Forwarded.

När insatsen är motiverad – och var den kan brista

Den visade metoden är alltid motiverad när Delphi-servicen inte längre lever „naken“ i LAN utan är en del av en produktiv kant: flera domäner, SSO/SAML-gränssnitt, publika API:er, multitenancy eller striktare revisionskrav. Den fallerar där man litar blint på Forwarded-headers eller proxy-topologier inte är dokumenterade (flera ingress-steg, Cloud-LB plus nginx plus sidecar). Då blir klient-IP och schema snabbt „något“.

En tydlig gräns: Om ni behöver komplexa trust-regler (CIDR, IPv6-nät, dynamiska LB-IP:er) bör ni utöka trust-prövningen (riktig IP-parsning, nätmasker) eller utforma infrastrukturen så att endast en definierad proxy kan nå Delphi-porten (brandvägg/säkerhetsgrupper). I slutändan är det oftast det mer robusta driftsbeslutet.

Slutsats: Att drifta en Reverse Proxy med nginx och Delphi på ett ordnat sätt betyder att hantera „Forwarded“ korrekt

En Reverse Proxy med nginx är för Delphi-REST-Server en god standardkomponent – men först den korrekta hanteringen av Forwarded och X-Forwarded-* gör uppsättningen stabil i drift. Kärnan är enkel: acceptera bara headers från betrodda proxys, härled Client-IP/Scheme/Host konsekvent och dra denna bas igenom omdirigeringar, loggning och säkerhetskontroller. Med snippeten ovan har ni en ren, legacy-kompatibel grund som går att integrera i WebBroker, Horse eller egna HTTP-servrar.

Om ni vill konsolidera ett befintligt Delphi-backend bakom nginx eller modernisera mot Delphi REST-API och REST-Server med en tydlig driftlinje, är en teknisk granskning av proxy-kedjan och headerutvärderingen ofta den snabbaste hävstången. Kontakta Net-Base för en kort teknisk bedömning.

I det tekniska sammanhanget spelar även Nginx Reverse Proxy och Forwarded-headers en viktig roll när integrationer, dataflöden och vidareutveckling måste samspela väl.

Diskutera projekt eller moderniseringsinitiativ med Net-Base.

Dela inlägg

Dela det här inlägget direkt

LinkedIn, X, XING, Facebook, WhatsApp och e‑post är omedelbart tillgängliga. För Instagram förbereder vi länken och en kort text direkt.

E-post

Instagram öppnas i en ny flik. Länken och korttexten kopieras till urklipp först.