Net-Base Ajakiri

23.05.2026

Reverse-proxy nginxiga ja Delphi: korrektne Forwarded-käsitlemine, reaalne kliendi-IP ja robustsed URL-baasid

Kui Delphi-REST-serverid töötavad nginxi taga, kipuvad Client-IP, HTTPS-tuvastus ja absoluutsed URL-id sageli valeks jääma. See koodilõik näitab robustset Forwarded-/X-Forwarded-päiste käsitlemist (sh Trust-Proxy-nimekiri), tüüpilisi nginx-seadeid ja silumisjuhiseid operatiivseks käitamiseks.

23.05.2026

Ein Reverse Proxy mit nginx und Delphi ist in der Praxis meist kein „nice to have“, sondern die saubere Trennung zwischen Internetkante und Applikation: TLS-Terminierung (HTTPS-Offloading), zentrale Header-/CORS-Regeln, Rate-Limits, einheitliche Logs, Blue/Green-Rollouts oder einfach das Hosting mehrerer Services unter einer Domain. Was dann gerne unterschätzt wird: Sobald nginx „davor“ sitzt, sieht der Delphi-Server nur noch die Proxy-IP, oft nur noch „http“ statt „https“ und generiert falsche absolute Links (Redirects, Callback-URLs, OpenAPI-Server-URL). Genau diese drei Punkte sorgen später für Debugging-Zeit im Betrieb.

See lähtekoodilõik näitab robustset mustrit, kuidas Delphi-is Forwarded ehk X-Forwarded-* korrektselt töödelda – kaasa arvatud Trust-Proxy-Liste (tähtis päiste võltsimise vastu) ja ühtne Request-Base-URL. Lisaks on olemas praktilised nginx-konfiguratsioonid ja juhised äärejuhtude kohta, näiteks WebSocketid, suured üleslaadimised ja timeout’id.

Warum Reverse Proxy-Setups Delphi-Server „verwirren“

nginx spricht als Reverse Proxy mit dem Delphi-Service typischerweise unverschlüsselt (HTTP) im internen Netz oder auf localhost, während der Client außen per HTTPS kommt. Ohne zusätzliche Header weiß Delphi nichts von:

  • Algne skeem (https vs. http) – relevant für Redirects und absolute URLs.
  • Algne host (kundenspezifische Domain, Port) – relevant für Multi-Tenant-Setups, CORS und Callback-URLs.
  • Algne kliendi-IP – relevant für Audit, Rate-Limits, Geo-Checks und Security-Auswertungen.

nginx kann diese Informationen über Header transportieren. Üblich sind X-Forwarded-For, X-Forwarded-Proto und X-Forwarded-Host; standardisiert ist zusätzlich der RFC-Header Forwarded. Wichtig: Diese Header sind aus Sicht der Applikation nicht automatisch vertrauenswürdig, weil ein Client sie selbst schicken kann – sie werden erst vertrauenswürdig, wenn sie von einem bekannten Proxy stammen.

nginx-Konfiguration: die minimal sinnvollen Proxy-Header

Ein solider Startpunkt (HTTP/1.1, Keep-Alive, Upgrade für WebSockets) sieht so aus. Das Snippet ist bewusst knapp gehalten; Sie ergänzen je nach Umgebung HSTS, Rate-Limits und 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;
  }
}

Eesmärk: Rakendusele edastatakse Host, kliendi-IP ja skeem usaldusväärselt. Piirtingimus: $proxy_add_x_forwarded_for lisab praeguse proksi-IP olemasolevasse ketti; see on hea mitme proksi seadistuse puhul, kuid muudab korrektse tõlgendamise Delphi-poolel veelgi olulisemaks. Püünis: Kui te nginx-is Host-päist ei sea, näeb Delphi võib-olla ainult upstream-hosti (127.0.0.1), mis rikub ümbersuunamisi ja origin-kontrolle.

Delphi koodilõik: Forwarded/X-Forwarded robustne tõlgendamine (usaldusproksi nimekirjaga)

Alljärgnev kood on teadlikult raamistikuneutraalne: see töötab minimaalse liidese (Header + RemoteIP) vastu ja on kohandatav WebBrokeri, RAD Serveri või Horse’iga. Olulised punktid:

  • Prioriteet: RFC Forwarded (kui olemas) eelistada X-Forwarded-*-ile.
  • Usaldus: Forwarded-päiseid tõlgendada ainult siis, kui otsene peer (RemoteIP) on teadaolev proksi.
  • Parsimine: arvestada IPv6, jutumärke, porte ja X-Forwarded-For’i aadressikette.
  • Väljund: baas-URL, mida saate kasutada absoluutsete linkide, ümbersuunamiste või OpenAPI jaoks.

unit Net-Base.ProxyForwarding;

interface

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

type
// Minimaalne adapterliides: rakendage seda WebBroker/Horse/jne jaoks.
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // otsene TCP-vastaspool (enamasti nginx)
end;

TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // nt. 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 võib olla „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 (Tähelepanu: alasti IPv6 ilma [] ei ole usaldusväärne)
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
// Näide: 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;

// Mitmed elemendid on komadega eraldatud; võtame esimest (kliendi lähim)
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 võib olla IP või „unknown“; IPv6 võib olla [] sees
V := V.Trim;
if SameText(V, ‚unknown‘) then
Continue;
// for=1.2.3.4:5678 võib esineda
if V.Contains(‚:‘) and (not V.StartsWith(‚[‚)) then
V := FirstCsvToken(V); // defensiivne
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
// Tõelise IPv6 normaliseerimise jaoks oleks vaja IP-parsimist; siin on see teadlikult pragmaatiline.
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; // Varuplaan: otsene vastaspool

// Ainult kui otsene vastaspool on tuntud proksi, analüüsime Forwarded päiseid.
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-* varuplaanina/täiendavalt
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;

// Vajadusel võtta Host päisest ‚Host‘ (kui proksi päiseid pole)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;

end.

Eesmärk: Te saate igast päringust konsistentse vaate ClientIP, Proto ja Host ning ka BaseUrl. Neid andmeid võite kasutada keskse logimise, turvalisuseotsuste (nt IP-Allowlist) ja linkide genereerimise jaoks.

Miks Trust-Proxy-loend on vajalik: Ilma usalduskontrollita võiks ründaja otse teie Delphi-pordini jõuda (valekonfiguratsioon, sisemine reiting, VPN) ja lihtsalt saata X-Forwarded-For: 127.0.0.1. Sellega muutuvad auditeerimisjäljed, Rate-Limits või „ainult sisevõrgus lubatud“ lõpp-punktid rünnatavaks. Usaldage Forwarded-päisteid ainult siis, kui otsene peer (RemoteIP) on proxy, mida kontrollite (nt 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).

Tõrkepunktid: IPv6 ilma nurksulgudeta ei ole Host:port-notatsioonis ühemõtteline. HTTP-Host-päises on IPv6 tavaliselt märgitud []-ga; järgige seda. Komplekssete IP-vahemike (CIDR) toetamiseks peate Trust-loendit laiendama (nt tõelise IP-parsimise kaudu).

Integratsioon WebBroker/Horse/RAD Serveriga: kuhu kood „ankurdub“

WebBrokeris (TWebRequest) tulevad päised tavaliselt läbi ContentFields või GetFieldByName, Remote-IP sõltub serveri back-endist. Horses (või teistes HTTP-raakides) on tavaliselt Req.Headers ja Remote-IP omadus. Oluline on põhimõte: RemoteIP peab olema TCP-kaaslane, mitte mingi päise väärtus.

Praktiliselt hästi toimiv lähenemine: looge teenuse käivitamisel konfiguratsioonist (INI/ENV) TTrustedProxyList, nt „127.0.0.1“ kohalike nginx-seadistuste jaoks või teie Load Balanceri IP. Seejärel kutsuge iga päringu puhul ResolveForwardedInfo ja kirjutage väljad oma struktureeritud logisse (JSON-Log, Syslog või Windows Event Log).

Tõrkeotsing käigus: nii leiate vead minutitega, mitte tundidega

Kui päringud tunduvad „veidrad“, ei ole süüdi tavaliselt Delphi-HTTP ise, vaid kombinatsioon proxy-päistest, redirect-loogikast ja timeout’idest. Kolm debug-kontrolli, mis igapäevaselt tasuvad end ära:

  1. Header-Dump (sihipärane): Logige 4xx/5xx korral lisaks Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent ja Request-URI. Aga ainult vigade korral – vastasel juhul muutub log kalliks ja ülevaatamatu.
  2. Base-URL kontroll: Kui redirectid või callback-URLid ebaõnnestuvad, logige ForwardedInfo.BaseUrl. Paljud vead on kohe nähtavad („http://127.0.0.1“ selle asemel, et „https://api…“).
  3. Timeoutide korrelatsioon: 504 proxylt ei ole sama mis Delphi-timeoute. nginx proxy_read_timeout ja Delphi-poolel olevad Idle-/Read-timeoutid peavad kokku sobima.

Erijuhtumid: WebSockets, streaming ja suured päringud

WebSockets nginx-i taga

WebSocketide jaoks peab nginx-i Upgrade ja Connection olema õigesti seadistatud. Lisaks ei tohi backend liiga vara ühendust sulgeda. Delphi-poolt on oluline, et teie WebSocket-komponent (või SSE/Streaming-lõpp-punkt) oskaks suhelda reverse proxydega ning et Heartbeats/Keep-Alives oleksid korrektselt implementeeritud.

Suured üleslaadimised ja 413-vead

Klassika: Delphi võtab üleslaadimise vastu, aga nginx blokeerib eelnevalt 413 Request Entity Too Large. Juhtige seda selgelt üle client_max_body_size ja kohandage Delphi-poolel Request-limiite. Protsessipõhiste tarkvaralahenduste puhul, mis töötlevad dokumente või pilte, ei ole see erand – see on igapäevatöö.

HTTPS-Offloading und „Secure Cookies“

Kui teie Delphi-teenus seab sessiooniküpsiseid, tuleb need välise HTTPS-i korral tavaliselt märgistada kui Secure. Kas teie rakendus seda teeb, sõltub tihti sellest, kas see „teab“, et algne päring oli HTTPS. Siin aitab järjekindel X-Forwarded-Proto/Forwarded-päiste analüüs.

Millal see pingutus end tasub – ja kus see võib ebaõnnestuda

Esitatud lähenemine tasub end ära alati, kui Delphi-teenus ei ela enam „alasti“ LAN-is, vaid on osa tootmisservast: mitu domeeni, SSO/SAML-liidesed, avalikud API-d, mitmeklienditugi või rangemad auditinõuded. See lähenemine kukub läbi seal, kus Forwarded-päistele usaldatakse pimesi või proxy-topoloogiad ei ole dokumenteeritud (mitmed ingress-kihid, Cloud-LB plus nginx plus Sidecar). Sellisel juhul muutuvad kliendi IP ja skeem kiiresti „midagi“.

Selge piir: kui vajate keerukaid usaldusreegleid (CIDR, IPv6-võrgud, dünaamilised LB-IP-d), peaksite usalduskontrolli laiendama (päris IP-parsimine, alamvõrgu maskid) või kujundama infrastruktuuri nii, et ainult määratud proxy pääseb Delphi-pordile ligi (Firewall/Security Groups). See on lõpptulemusena sageli töökindlam operatsioonide otsus.

Järeldus: Reverse-proxy nginxiga ja Delphi korrektselt pidamine tähendab „Forwarded õigesti käsitleda“

Reverse-proxy nginxiga on Delphi-REST-Server jaoks hea standardkomponent – kuid alles Forwarded ja X-Forwarded-* korrektne käsitlus teeb selle ülesseadistuse töös stabiilseks. Tuum on lihtne: päiseid aktsepteerida ainult usaldusväärsetelt proxy-delt, tuletada järjekindlalt Client-IP/Scheme/Host ja rakendada seda alusprintsiipi suunamistes, logimises ja turvakontrollides. Ülaltoodud näidisega on teil selleks puhas, legacy-sobiv alus, mida saab integreerida WebBrokeri, Horse’i või enda HTTP-serveritesse.

Kui soovite olemasolevat Delphi-backendit nginxi taha konsolideerida või suunata moderniseerimist Delphi REST-API ja REST-Server suunas koos selge operatsiooniliiniga, on proxy-ahela ja päiste analüüsi tehniline review sageli kiireim tõhus käik. Võtke ühendust Net-Base lühikese tehnilise hinnangu saamiseks.

Asjatundlikus kontekstis mängivad ka Nginx Reverse Proxy ja Forwarded-päised olulist rolli, kui integratsioonid, andmevood ja edasiarendus peavad puhtalt koos töötama.

Arutage projekti või moderniseerimisettevõtmist koos Net-Base.

Jaga postitust

Jaga seda postitust otse

LinkedIn, X, XING, Facebook, WhatsApp ja e-post on kohe saadaval. Instagrami jaoks valmistame kohe lingi ja lühiteksti ette.

e-post

Instagram avatakse uues vahekaardis. Link ja lühitekst kopeeritakse eelnevalt lõikepuhvrisse.