Net-Base Magasin

23.05.2026

Reverse proxy med nginx og Delphi: ryddig Forwarded-handtering, ekte klient-IP og robuste URL-baser

Når Delphi-REST-serverar køyrer bak nginx, blir ofte Client-IP, HTTPS-deteksjon og absolutte URL-ar feil. Denne kodesnutten viser robust Forwarded-/X-Forwarded-handtering (inkl. ei liste over betrodde proxyar), typiske nginx-innstillingar og feilsøkingstips for drift.

23.05.2026

Ein Reverse Proxy mit nginx und Delphi er i praksis som regel ikkje eit „nice to have“, men den ryddige skiljet mellom internettkanten og applikasjonen: TLS-terminering (HTTPS-offloading), sentrale Header-/CORS-reglar, Rate-Limits, einheitlege Logs, Blue/Green-Rollouts eller enkelt hosting av fleire tenester under eit domene. Det som gjerne blir undervurdert då: Når nginx sit „framfor“, ser Delphi-serveren berre proxy-IP-en, ofte berre „http“ i staden for „https“ og genererer feil absoluttlenker (Redirects, Callback-URLs, OpenAPI-Server-URL). Desse tre punkta fører seinare til tidkrevjande feilsøking i drift.

Dette source-schnipslet viser eit robust mønster for korleis du i Delphi Forwarded eller X-Forwarded-* kan tolke på ein ryddig måte – inklusive ei Trust-Proxy-Liste (viktig mot Header-Spoofing) og ei konsekvent Request-Base-URL. I tillegg finn du praksisnære nginx-konfigurasjonar og merknader om randtilfelle som WebSockets, store opplastingar og Timeouts.

Kvifor Reverse Proxy-oppsett forvirrar Delphi-serverar

nginx kommuniserer som Reverse Proxy med Delphi-tenesta typisk ukryptert (HTTP) i det interne nettverket eller på localhost, medan klienten kjem utanfrå via HTTPS. Uten ekstra Headerar veit Delphi ingenting om:

  • Original-Schema (https vs. http) – relevant for Redirects og absolutte URL-ar.
  • Original-Host (kundespesifikt domene, Port) – relevant for Multi-Tenant-Setups, CORS og Callback-URL-ar.
  • Original-Client-IP – relevant for revisjon, Rate-Limits, Geo-Checks og sikkerheitsanalysar.

nginx kan transportere desse informasjonane via Headerar. Vanleg er X-Forwarded-For, X-Forwarded-Proto og X-Forwarded-Host; standardisert er i tillegg RFC-Headeren Forwarded. Viktig: Desse Headerane er frå applikasjonen sitt syn ikkje automatisk til å stole på, fordi ein klient kan sende dei sjølv – dei blir først til å stole på når dei kjem frå ein kjend proxy.

nginx-Konfiguration: die minimal sinnvollen Proxy-Header

Eit solid utgangspunkt (HTTP/1.1, Keep-Alive, Upgrade for WebSockets) ser slik ut. Dette Snippetet er medvite kortfatta; du supplerer etter omgjevnaden med HSTS, Rate-Limits og Access-Logs.

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

Formål: Applikasjonen får Host, klient-IP og schema påliteleg vidareleidd. Randvilkår: $proxy_add_x_forwarded_for legg den aktuelle proxy-IP-en til ei eventuell eksisterande kjede; dette er bra for multi-proxy-oppsett, men gjer den korrekte tolkinga på Delphi-sida endå viktigare. Fallgruve: Hvis De i nginx ikkje set Host-headeren, ser Delphi kanskje berre upstream-vert (127.0.0.1), noko som bryt omdirigeringar og origin-sjekkar.

Delphi Source-snutt: Robust tolking av Forwarded/X-Forwarded (med Trust-Proxy-liste)

Følgjande kode er med vilje rammeverksnøytral: Den arbeider mot eit minimalt grensesnitt (Header + RemoteIP) og kan tilpassast i WebBroker, RAD Server eller Horse. Hovudpunkt:

  • Prioritet: RFC Forwarded (dersom tilstades) framfor X-Forwarded-*.
  • Trust: Forwarded-header skal berre tolkast når den direkte peeren (RemoteIP) er ein kjend proxy.
  • Parsing: IPv6, anførselstegn, portar og kjeder i X-Forwarded-For blir tekne i betraktning.
  • Output: ei base-URL som De kan bruke for absolutte lenker, omdirigeringar eller OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

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

type
  // Minst mogleg adaptergrensesnitt: implementer det for WebBroker/Horse/osv.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // direkte TCP-motpart (vanlegvis nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // t.d. 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 kan vere "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 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 (Merk: ved naken IPv6 utan [] er dette ikkje påliteleg)
  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
  // Døme: 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;

  // Fleire element er kommadelte; vi tek det første (nærast klienten)
  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 kan vere ei IP eller "unknown"; IPv6 kan stå i []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 kan førekome
      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
  // For ekte IPv6-normalisering ville ein trenge IP-parsing; her er vi medvite pragmatiske.
  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 motpart

  // Vi tolkar Forwarded-header berre dersom den direkte motparten er ein kjend proxy.
  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-* som fallback/utfylling
    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;

  // I naudtilfelle hentar vi Host frå "Host"-headeren (dersom det ikkje finst proxy-header)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Formål: Du får frå kvar request ei konsistent oversikt over ClientIP, Proto og Host samt ei BaseUrl. Desse opplysningane kan du bruke sentralt for logging, sikkerheitsavgjerder (t.d. IP-Allowlist) og lenke-generering.

Kvarfor Trust-Proxy-lista er nødvendig: Uten ei trust-sjekk kunne ein angripar nådd direkte din Delphi-port (feilkonfigurasjon, internt routing, VPN) og enkelt sende X-Forwarded-For: 127.0.0.1. Då ville audit-trails, rate-limits eller „berre internt tillate“-endepunkt vere sårbare. Stol på Forwarded-headerar berre når den direkte motparten (RemoteIP) er ein proxy du kontrollerer (t.d. 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).

Fallgruver: IPv6 utan hakeparentesar er i Host:port-notasjon ikkje entydig. I HTTP-Host-headeren er IPv6 som oftast oppført i []; hald deg til det. For komplekse IP-område (CIDR) må du utvide trust-lista (t.d. med ein reell IP-parsing).

Integrasjon i WebBroker/Horse/RAD Server: kvar koden „andockar“

I WebBroker (TWebRequest) kjem headerar typisk via ContentFields eller GetFieldByName, og Remote-IP avhengig av server-backend. I Horse (eller andre HTTP-rammeverk) finst vanlegvis Req.Headers og ei Remote-IP-eigenskap. Viktig prinsipp: RemoteIP må vere TCP-motparten, ikkje ein vilkårleg header-verdi.

Praktisk erfart: Opprett ved tenestestart ei TTrustedProxyList frå konfigurasjon (INI/ENV), t.d. „127.0.0.1“ for lokale nginx-oppsett eller IP-en til load-balanceren din. Kall så ResolveForwardedInfo per request og skriv felta til strukturert logging (JSON-logg, Syslog eller Windows Event Log).

Debugging i drift: slik finn du feil på minutt i staden for timar

Når requests verkar „komiske“, skuldast det sjeldan Delphi-HTTP sjølv, men heller ei kombinasjon av proxy-header, redirect-logikk og timeouts. Tre debug-sjekkar som lønner seg i kvardagen:

  1. Header-dump (målretta): Logg ved 4xx/5xx i tillegg Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent og Request-URI. Men berre ved feil — elles blir loggen dyr og uoversiktleg.
  2. Sjekk Base-URL: Når redirects eller callback-URLar feilar, logg ForwardedInfo.BaseUrl. Mange feil blir straks synlege („http://127.0.0.1″ i staden for „https://api…“).
  3. Timeout-korrelasjon: Ein 504 frå proxyen er ikkje det same som eit Delphi-timeout. nginx proxy_read_timeout og Delphi-sidelege idle-/read-timeoutar må passe saman.

Kanttilfelle: WebSockets, streaming og store requests

WebSockets bak nginx

For WebSockets krev nginx korrekte Upgrade og Connection-header. I tillegg må backend ikkje stengje „for tidleg“. På Delphi-sida er det relevant at WebSocket-komponenten din (eller eit SSE/streaming-endepunkt) kan handtere reverse-proxyar og at heartbeats/keep-alives er riktig implementert.

Store opplastingar og 413-feil

Eit klassisk tilfelle: Delphi aksepterer eit opplasting, men nginx blokkjerer det på førehand med 413 Request Entity Too Large. Styr dette eksplisitt gjennom client_max_body_size og tilpass Delphi-sidelege request-grensar. For proksessnære programvareløysingar med dokument- eller bilete-data er dette normal drift, ikkje eit unntak.

HTTPS-Offloading og „Secure Cookies“

Hvis din Delphi-teneste set Session-Cookies, må desse ved eksternt HTTPS som regel markerast som Secure. Om applikasjonen din gjer det, heng ofte på om ho «veit» at den opphavlege førespurnaden var HTTPS. Nøyaktig her hjelper konsekvent tolking av X-Forwarded-Proto/Forwarded.

Når innsatsen løner seg – og kvar ho kan svikte

Den viste tilnærminga løner seg alltid når Delphi-tenesten ikkje lenger ligg «naken» i LAN-et, men er ein del av ein produktiv kant: fleire domener, SSO-/SAML-grensesnitt, offentlege APIs, fleirtanantskap eller strengare revisjonskrav. Den sviktar der ein blindt stolar på Forwarded-header eller når proxy-topologiar ikkje er dokumenterte (fleire Ingress-lag, Cloud-LB pluss nginx pluss Sidecar). Då blir klient-IP og protokoll raskt «noko».

Eit klart skilje: Om du treng komplekse tillitsreglar (CIDR, IPv6-nett, dynamiske LB-IPar), bør du utvide tillitssjekken (ekte IP-parsing, nettmasker) eller utforme infrastrukturen slik at berre ein definert proxy kan nå Delphi-porten (Firewall/Security Groups). Det er til slutt som regel den mest robuste driftsavgjerda.

Konklusjon: Å drifte ein Reverse Proxy med nginx og Delphi på ein ryddig måte betyr «gjera Forwarded riktig»

Ein Reverse Proxy med nginx er for Delphi-REST-Server ein god standardbaustein – men fyrst den korrekte handsaminga av Forwarded og X-Forwarded-* gjer oppsettet stabilt i drift. Kjernen er enkel: godta berre header frå pålitelege proxiar, utlei klient-IP/protokoll/host konsistent og før denne basisen gjennom i Redirects, Logging og Security-Checks. Med snippetet ovanfor har du eit reint, legacy-taugleg fundament som kan integrerast i WebBroker, Horse eller eigne HTTP-serverar.

Om du vil konsolidere eit eksisterande Delphi-backend bak nginx eller modernisere mot Delphi REST-API og REST-Server med ei klar driftslinje, er ein teknisk gjennomgang av proxy-kjeda og header-tolking ofte det raskaste tiltaket. Kontakt Net-Base for ei kort teknisk vurdering.

I fagleg samanheng spelar også Nginx Reverse Proxy og Forwarded-Header ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må samspela på ein ryddig måte.

Drøft prosjekt eller moderniseringsprosjekt med Net-Base.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e-post er tilgjengelege med ein gong. For Instagram klargjer vi lenke og kort tekst med det same.

E-post

Instagram opnar i ein ny fane. Lenkje og kort tekst blir kopiert til utklippstavla på førehand.