Net-Base Dergi

23.05.2026

nginx ve Delphi ile ters proxy: doğru Forwarded başlık işleme, gerçek istemci IP'si ve sağlam URL tabanları

Eğer Delphi-REST-sunucular nginx arkasında çalışıyorsa, genellikle Client-IP, HTTPS algılama ve mutlak URL'ler bozulur. Bu kaynak kod parçası, güvenilen proxy listesi dahil olmak üzere sağlam bir Forwarded-/X-Forwarded işleme, tipik nginx ayarları ve işletim için hata ayıklama notlarını gösterir.

23.05.2026

nginx ve Delphi ile bir Reverse Proxy uygulamada genellikle bir „nice to have“ değil, internet kenarı ile uygulama arasındaki temiz ayrımdır: TLS sonlandırma (HTTPS-Offloading), merkezi Header-/CORS kuralları, Rate-Limits, tutarlı loglar, Blue/Green roll-out’lar veya basitçe bir alan altında birden fazla servisin barındırılması. Sıkça hafife alınan nokta şudur: nginx öne geçtiğinde, Delphi sunucusu artık yalnızca proxy IP’sini görür, çoğu zaman artık „https“ yerine sadece „http“ görür ve yanlış mutlak linkler üretir (Redirects, Callback-URL’leri, OpenAPI-Server-URL). Bu üç nokta işletmede sonradan uzun debug sürelerine yol açar.

Bu kaynak kesiti, Delphi içinde Forwarded veya X-Forwarded-* başlıklarını nasıl sağlam şekilde değerlendireceğinize dair dayanıklı bir desen gösterir – Trust-Proxy-Liste (Header-Spoofing’e karşı önemli) ve tutarlı bir Request-Base-URL dahil. Buna ek olarak pratik nginx konfigürasyonları ve WebSockets, büyük yüklemeler ve timeouts gibi kenar durumlara ilişkin notlar içerir.

Neden Reverse Proxy kurulumları Delphi sunucularını „kandırır“

nginx, Reverse Proxy olarak Delphi servisiyle tipik olarak iç ağda veya localhost üzerinde şifrelenmemiş (HTTP) olarak konuşur; oysa dışarıdaki istemci HTTPS ile gelir. Ek başlıklar olmadan Delphi şu konulardan habersizdir:

  • Original-Schema (https vs. http) – Redirects ve mutlak URL’ler için önemli.
  • Original-Host (müşteri-özel domain, port) – Multi-Tenant kurulumlar, CORS ve Callback-URL’leri için önemli.
  • Original-Client-IP – Audit, Rate-Limits, Geo-Check’ler ve güvenlik analizleri için önemli.

nginx bu bilgileri Header’lar aracılığıyla taşıyabilir. Yaygın olanlar X-Forwarded-For, X-Forwarded-Proto ve X-Forwarded-Host’tur; ayrıca standartlaştırılmış RFC Header Forwarded vardır. Önemli: Bu Header’lar uygulama açısından otomatik olarak güvenilir değildir, çünkü bir istemci bunları kendisi de gönderebilir – ancak bilinen bir proxy’den geliyorlarsa güvenilir kabul edilirler.

nginx-Konfiguration: die minimal sinnvollen Proxy-Header

Sağlam bir başlangıç noktası (HTTP/1.1, Keep-Alive, WebSockets için Upgrade) şu şekildedir. Snippet kasıtlı olarak kısa tutulmuştur; ortama bağlı olarak HSTS, Rate-Limits ve Access-Logs eklersiniz.

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

Amaç: Uygulama Host, Client-IP ve şemayı güvenilir şekilde iletilmiş olarak alır. Koşul: $proxy_add_x_forwarded_for mevcut proxy IP’sini varsa var olan bir zincire ekler; bu çoklu proxy kurulumları için uygundur, ancak Delphi tarafında doğru değerlendirmeyi daha kritik hale getirir. Uyarı: nginx içinde Host-başlığını ayarlamazsanız, Delphi muhtemelen yalnızca Upstream-Host (127.0.0.1) görür; bu da yönlendirmeleri ve Origin kontrollerini bozabilir.

Delphi Kaynak-Schnipsel: Forwarded/X-Forwarded güvenli şekilde değerlendirme (Trust-Proxy-Liste ile)

Aşağıdaki kod kasıtlı olarak çerçeveden bağımsız tutulmuştur: Minimal bir arayüz (Header + RemoteIP) üzerinden çalışır ve WebBroker, RAD Server veya Horse’a uyarlanabilir. Temel noktalar:

  • Öncelik: RFC Forwarded (varsa) X-Forwarded-*‚den önce.
  • Güven: Forwarded-Header’ları yalnızca doğrudan eş (RemoteIP) bilinen bir proxy ise değerlendirin.
  • Ayrıştırma: IPv6, tırnak işaretleri, portlar ve X-Forwarded-For içindeki zincirleri dikkate alın.
  • Çıktı: mutlak bağlantılar, yönlendirmeler veya OpenAPI için kullanabileceğiniz bir temel URL.
Delphi
unit Net-Base.ProxyForwarding;

interface

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

type
  // Minimal bir adaptör arayüzü: WebBroker/Horse/etc. için bunu uygulayın.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // doğrudan TCP karşı uç (çoğunlukla nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // örn. 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 "client, proxy1, proxy2" biçiminde olabilir
  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 köşeli parantez içinde: [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 (Dikkat: köşeli parantezsiz çıplak IPv6'da güvenilir değil)
  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
  // Örnek: 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;

  // Birden çok öğe virgülle ayrılır; ilkini (istemciye en yakın olanı) alıyoruz
  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 bir IP veya "unknown" olabilir; IPv6 köşeli parantez içinde olabilir
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 görülebilir
      if V.Contains(':') and (not V.StartsWith('[')) then
        V := FirstCsvToken(V); // tedbir amaçlı
      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
  // Gerçek IPv6 normalizasyonu için bir IP ayrıştırması gerekir; burada kasıtlı olarak pragmatik.
  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; // Yedek: doğrudan karşı uç

  // Sadece doğrudan karşı uç bilinen bir proxy ise Forwarded başlıklarını değerlendiriyoruz.
  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-* yedek/tamamlayıcı olarak
    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;

  // Gerekirse Host'u "Host" başlığından al (proxy başlığı yoksa)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Amaç: Her istekte ClientIP, Proto ve Host ile bir BaseUrl hakkında tutarlı bir görünüm elde edersiniz. Bu bilgileri merkezi olarak günlükleme, güvenlik kararları (ör. IP-Allowlist) ve bağlantı oluşturma için kullanabilirsiniz.

Trust-Proxy listesinin gerekliliği: Güven denetimi olmadan bir saldırgan doğrudan Delphi-portunuza erişebilir (yanlış yapılandırma, dahili yönlendirme, VPN) ve basitçe X-Forwarded-For: 127.0.0.1 gönderebilir. Böylece audit-trail’ler, rate-limit’ler veya “sadece dahiliye açık” uç noktalar saldırıya açık hale gelir. Forwarded başlıklarına yalnızca doğrudan karşı uç (RemoteIP) sizin kontrolünüzde olan bir proxy ise güvenin (ör. 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).

Dikkat edilmesi gerekenler: Köşeli parantezsiz IPv6, Host:port notasyonunda belirsizdir. HTTP Host başlığında IPv6 genellikle [] içinde yazılır; buna uyun. Karmaşık IP aralıkları (CIDR) için Trust listesini genişletmeniz gerekir (örn. gerçek bir IP ayrıştırması ile).

WebBroker/Horse/RAD Server ile entegrasyon: kodun „andockt“ olduğu yer

WebBroker (TWebRequest) içinde header’lar tipik olarak ContentFields veya GetFieldByName üzerinden gelir; Remote-IP sunucu backend’ine bağlıdır. Horse’ta (veya diğer HTTP-framework’lerde) genellikle Req.Headers ve bir Remote-IP özelliği bulunur. Önemli ilke şudur: RemoteIP, TCP karşı tarafı olmalıdır, herhangi bir header değeri değil.

Pratikte kanıtlanmış yaklaşım: Servis başlatılırken konfigürasyondan (INI/ENV) bir TTrustedProxyList oluşturun; örn. yerel nginx kurulumları için „127.0.0.1“ veya Load Balancer IP’niz. Ardından her istek için ResolveForwardedInfo çağırın ve alanları yapılandırılmış loglamanıza yazın (JSON-Log, Syslog veya Windows Event Log).

İşletmede hata ayıklama: hataları saatler yerine dakikalar içinde bulma

İstekler “tuhaf” görünüyorsa, bunun nedeni nadiren Delphi-HTTP’in kendisidir; daha ziyade proxy başlıkları, yönlendirme mantığı ve zaman aşımlarının bir kombinasyonudur. Günlük kullanımda işe yarayan üç debug kontrolü:

  1. Header-Dump (hedefli): 4xx/5xx durumlarında ayrıca Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent ve Request-URI değerlerini loglayın. Ancak yalnızca hatalarda — aksi takdirde log maliyetli ve karmaşık hale gelir.
  2. Base-URL kontrolü: Yönlendirmeler veya callback-URL’leri başarısız olduğunda ForwardedInfo.BaseUrl değerini loglayın. Birçok hata hemen görünür („http://127.0.0.1“ yerine „https://api…“).
  3. Zaman aşımı korelasyonu: Proxy’den gelen bir 504, bir Delphi-zaman aşımı ile aynı değildir. nginx proxy_read_timeout ile Delphi tarafındaki Idle-/Read-timeout’ların uyumlu olması gerekir.

Kenar durumları: WebSockets, streaming ve büyük istekler

nginx arkasındaki WebSockets

WebSockets için nginx’in Upgrade ve Connection başlıklarını doğru ayarlaması gerekir. Ek olarak backend bağlantıyı „çok erken“ kapatmamalıdır. Delphi tarafında önemli olan, WebSocket bileşeninizin (veya bir SSE/Streaming uç noktasının) reverse proxy’lerle başa çıkabilmesi ve Heartbeats/Keep-Alives’in düzgün uygulanmış olmasıdır.

Büyük yüklemeler ve 413 hatası

Klasik durum: Delphi bir yüklemeyi kabul eder, ancak nginx önceden 413 Request Entity Too Large ile engeller. Bunu açıkça client_max_body_size ile yönetin ve Delphi tarafında istek limitlerini ayarlayın. Belge veya görsel verilerle çalışan süreç-odaklı yazılım çözümleri için bu bir istisna değil, normal işletimdir.

HTTPS-Offloading und „Secure Cookies“

Eğer Delphi servisi oturum çerezleri (Session-Cookies) oluşturuyorsa, bu çerezler dışarıdan gelen HTTPS bağlantılarında genellikle Secure olarak işaretlenmelidir. Uygulamanızın bunu yapıp yapmadığı çoğu zaman orijinal isteğin HTTPS olduğunu „bildiğine“ bağlıdır. Tam da bu noktada X-Forwarded-Proto/Forwarded başlıklarının tutarlı değerlendirilmesi yardımcı olur.

Ne zaman çabaya değer – ve nerede tersine dönebilir

Gösterilen yaklaşım, Delphi servisi artık LAN içinde „yalın“ halde durmayıp üretim kenarının bir parçası olduğunda her zaman işe yarar: birden fazla domain, SSO/SAML ara yüzleri, Public APIs, çoklu kiracılık veya daha sıkı denetim gereksinimleri. Yaklaşım, Forwarded başlıklarına körü körüne güvenildiğinde veya proxy topolojileri belgelenmediğinde (birden fazla Ingress katmanı, Cloud-LB artı nginx artı Sidecar) işlevini yitirir. Bu durumda istemci IP’si ve şema hızla „herhangi bir şey“ olur.

Kesin bir sınır: Eğer karmaşık güven kurallarına (CIDR, IPv6 ağları, dinamik LB IP’leri) ihtiyacınız varsa, güven doğrulamasını genişletmelisiniz (gerçek IP-parsing, ağ maskeleri) veya altyapıyı yalnızca tanımlı bir proxy’nin Delphi portuna erişebileceği şekilde tasarlamalısınız (Firewall/Security Groups). Sonuçta bu genellikle daha sağlam bir işletme kararıdır.

Fazit: nginx ile Reverse Proxy ve Delphi’yi düzgün işletmek, “Forwarded”ı doğru yapmak demektir

nginx ile bir Reverse Proxy, Delphi-REST-Server için iyi bir standart yapı taşıdır – ancak işletmede kurulumu stabil kılan şey Forwarded ve X-Forwarded-* başlıklarının doğru işlenmesidir. Özü basittir: Başlıkları yalnızca güvenilir proxy’lerden kabul edin, istemci IP’si/şema/host’u tutarlı biçimde türetin ve bu temeli yönlendirmelerde, loglamada ve güvenlik kontrollerinde uygulayın. Yukarıdaki snippet ile bu amaç için WebBroker, Horse veya kendi HTTP sunucularınıza entegre edilebilen temiz, legacy-uyumlu bir temel elde edersiniz.

Eğer mevcut bir Delphi backend’i nginx arkasında konsolide etmek veya açık işletme sınırına sahip bir Delphi REST-API und REST-Server yönünde modernize etmek istiyorsanız, proxy zincirinin ve başlık değerlendirmesinin teknik bir incelemesi genellikle en hızlı etkiyi sağlayan adımdır. Kısa bir teknik değerlendirme için Net-Base ile iletişime geçin.

Uzmanlık alanında, entegrasyonlar, veri akışları ve yazılımın ileriye dönük geliştirilmesi temiz şekilde iç içe geçmesi gerektiğinde Nginx Reverse Proxy ve Forwarded başlıkları da önemli bir rol oynar.

Proje veya modernizasyon girişimini Net-Base ile görüşün.

Gönderiyi paylaş

Bu gönderiyi doğrudan paylaş

LinkedIn, X, XING, Facebook, WhatsApp ve e-posta hemen kullanılabilir. Instagram için bağlantı ve kısa metni doğrudan hazırlıyoruz.

E-posta

Instagram yeni bir sekmede açılır. Bağlantı ve kısa metin önceden panoya kopyalanır.