Net-Base Журнал

23.05.2026

Зворотний проксі з nginx та Delphi: коректна обробка заголовка Forwarded, фактична IP-адреса клієнта та надійні базові URL-адреси

Якщо Delphi-REST-сервери працюють за nginx, значення Client-IP, виявлення HTTPS і абсолютні URL-адреси часто визначаються неправильно. Цей фрагмент вихідного коду демонструє надійну обробку заголовків Forwarded / X-Forwarded (включно зі списком довірених проксі), типові налаштування nginx і вказівки з налагодження для експлуатації.

23.05.2026

Інструмент Reverse Proxy з nginx і Delphi на практиці зазвичай не є «nice to have», а забезпечує чітке розділення між інтернет-краєм і аплікацією: термінація TLS (HTTPS-offloading), центральні правила заголовків/CORS, обмеження швидкості запитів, уніфіковані логи, blue/green‑розгортання або просто хостинг кількох сервісів під однією доменною зоною. Те, що часто недооцінюють: як тільки nginx стоїть «перед» сервером Delphi, цей сервер бачить лише IP проксі, часто тільки «http» замість «https» і генерує некоректні абсолютні посилання (редиректи, callback-URL, URL сервера OpenAPI). Саме ці три моменти пізніше створюють час на налагодження в експлуатації.

Цей фрагмент коду показує стійкий патерн, як у Delphi коректно розбирати Forwarded та X-Forwarded-* — включно зі списком довірених проксі (важливо проти підробки заголовків) і послідовною Request-Base-URL. Також наведені практичні конфігурації nginx і вказівки щодо крайових випадків, як-от WebSockets, великі завантаження і таймаути.

Чому налаштування Reverse Proxy «заплутують» сервери Delphi

nginx як Reverse Proxy зазвичай спілкується з сервісом Delphi незашифровано (HTTP) у внутрішній мережі або на localhost, тоді як клієнт звертається ззовні через HTTPS. Без додаткових заголовків Delphi не знає про:

  • Оригінальна схема (https vs. http) – важливо для редиректів та абсолютних URL-ів.
  • Оригінальний хост (домен клієнта, порт) – важливо для мультиорендних налаштувань, CORS і callback-URL.
  • Оригінальна IP клієнта – важливо для аудиту, обмежень частоти запитів, геоперевірок та аналізу безпеки.

nginx може передавати цю інформацію через заголовки. Звично використовують X-Forwarded-For, X-Forwarded-Proto та X-Forwarded-Host; додатково стандартизований RFC-заголовок Forwarded. Важливо: ці заголовки з точки зору аплікації не є автоматично довіреними, бо їх може надіслати сам клієнт — вони стають довіреними лише коли походять від відомого проксі.

nginx-Konfiguration: die minimal sinnvollen Proxy-Header

Надійна відправна точка (HTTP/1.1, Keep-Alive, Upgrade для WebSockets) виглядає так. Фрагмент навмисно стислий; ви доповнюєте залежно від середовища HSTS, обмеженнями швидкості запитів і журналами доступу.

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

Мета: Аплікація отримує Host, IP клієнта та схему надійно переданими. Передумова: $proxy_add_x_forwarded_for приписує поточну IP проксі до можливо наявного ланцюжка; це корисно для Multi-Proxy-Setup-ів, але робить правильний розбір на боці Delphi тим важливішим. Підводний камінь: Якщо Ви в nginx не встановите заголовок Host, Delphi може бачити лише Upstream-Host (127.0.0.1), що ламає Redirects та Origin-Checks.

Delphi Зразок вихідного коду: надійна обробка Forwarded/X-Forwarded (з переліком довірених проксі)

Наступний код умисно нейтральний щодо фреймворку: він працює проти мінімального інтерфейсу (Header + RemoteIP) і може бути адаптований для WebBroker, RAD Server або Horse. Основні пункти:

  • Пріоритет: RFC Forwarded (якщо наявний) перед X-Forwarded-*.
  • Довіра: розбирати заголовки Forwarded лише якщо прямий пір (RemoteIP) — відомий проксі.
  • Розбір: враховувати IPv6, лапки, порти та ланцюжки в X-Forwarded-For.
  • Вивід: базову URL-адресу, яку Ви можете використовувати для абсолютних посилань, Redirects або OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

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

type
  // Мінімальний інтерфейс адаптера: реалізуйте його для WebBroker/Horse тощо
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // пряма TCP-сторона з'єднання (зазвичай nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // наприклад 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"
  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 в []: [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:port (Увага: для голого IPv6 без [] ненадійно)
  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
  // Приклад: 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;

  // Кілька елементів розділені комами; беремо перший (найближчий до клієнта)
  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 може бути IP або "unknown"; IPv6 може бути в []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // зустрічається for=1.2.3.4:5678
      if V.Contains(':') and (not V.StartsWith('[')) then
        V := FirstCsvToken(V); // запобіжно
      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
  // Для справжньої нормалізації IPv6 потрібне розбору IP; тут свідомо прагматично.
  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; // Резервний варіант: пряма протилежна сторона

  // Лише якщо пряма протилежна сторона є відомим проксі, ми аналізуємо заголовки 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-* як запасний/доповнювальний
    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;

  // У крайньому разі взяти Host з заголовка "Host" (якщо немає проксі-заголовків)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Призначення: З кожного запиту ви отримуєте послідовний погляд на ClientIP, Proto і Host а також на BaseUrl. Цю інформацію ви можете використовувати централізовано для логування, прийняття рішень з безпеки (наприклад, IP-Allowlist) та генерації посилань.

Чому потрібен список Trust-Proxy: Без перевірки довіри зловмисник може напряму дістатися до вашого Delphi-порту (помилка конфігурації, внутрішнє маршрутування, VPN) і просто відправити X-Forwarded-For: 127.0.0.1. Це робить вразливими audit-trails, rate-limits або кінцеві точки «лише для внутрішнього використання». Довіряйте Forwarded-заголовкам тільки якщо прямий пір (RemoteIP) — це проксі, яким ви контролюєте (наприклад, 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).

Підводні камені: IPv6 без квадратних дужок у нотації Host:port не однозначний. В HTTP-Host-заголовку IPv6 зазвичай зазначається в []; дотримуйтеся цього. Для складніших діапазонів IP (CIDR) вам доведеться розширити список довіри (наприклад, за рахунок реального парсингу IP).

Інтеграція в WebBroker/Horse/RAD Server: де код «підключається»

У WebBroker (TWebRequest) заголовки зазвичай доступні через ContentFields або GetFieldByName, RemoteIP залежить від серверного бекенду. В Horse (або інших HTTP-фреймворках) зазвичай є Req.Headers та властивість RemoteIP. Важливий принцип: RemoteIP має бути TCP‑співрозмовником, а не якимось значенням із заголовка.

Практично випробувано: створіть при старті сервісу TTrustedProxyList з конфігурації (INI/ENV), наприклад «127.0.0.1» для локальних nginx‑налаштувань або IP вашого Load Balancer. Потім викликайте ResolveForwardedInfo для кожного запиту і записуйте поля у ваше структуроване логування (JSON-Log, Syslog або Windows Event Log).

Налагодження в експлуатації: як знаходити помилки за хвилини замість годин

Якщо запити виглядають «дивно», проблема рідко в самому Delphi-HTTP, скоріше в поєднанні proxy‑заголовків, логіки редиректів та таймаутів. Три перевірки для налагодження, що економлять час у повсякденній роботі:

  1. Дамп заголовків (цілеспрямовано): логируйте при 4xx/5xx додатково Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent та Request-URI. Але тільки при помилках — інакше лог стане дорогим і нечитабельним.
  2. Перевірка Base-URL: якщо редиректи або callback-URL не працюють, логируйте ForwardedInfo.BaseUrl. Багато помилок видно одразу («http://127.0.0.1» замість «https://api…»).
  3. Кореляція таймаутів: 504 від проксі — це не те саме, що таймаут Delphi. nginx proxy_read_timeout і Delphi‑сторонні Idle-/Read‑таймаути повинні узгоджуватися.

Особливі випадки: WebSockets, Streaming та великі запити

WebSockets за nginx

Для WebSockets nginx потребує коректних заголовків Upgrade і Connection. Додатково бекенд не повинен закривати з’єднання «занадто рано». На боці Delphi важливо, щоб ваша WebSocket‑компонента (або SSE/стрімінговий ендпоінт) могла працювати з реверс‑проксі і щоб Heartbeats/Keep‑Alives були реалізовані чисто.

Великі завантаження та помилки 413

Класика: Delphi приймає завантаження, але nginx блокує його раніше з 413 Request Entity Too Large. Керуйте цим явно через client_max_body_size і налаштуйте на боці Delphi ліміти запитів. Для процесоорієнтованих програмних рішень з документами або зображеннями це не виняток, а штатна робота.

HTTPS-Offloading und „Secure Cookies“

Якщо ваш Delphi-сервіс встановлює сесійні Cookie, то при зовнішньому HTTPS вони, як правило, мають бути позначені як Secure. Чи робить це ваша аплікація, часто залежить від того, чи вона «знає», що початковий запит був HTTPS. Саме тут допомагає послідовний аналіз X-Forwarded-Proto/Forwarded.

Коли ці зусилля виправдані — і де вони можуть дати збій

Показаний підхід виправданий завжди, коли Delphi-сервіс більше не знаходиться «голим» у LAN, а стає частиною продуктивного краю: кілька доменів, SSO/SAML-інтерфейси, публічні API, багатоклієнтність або жорсткіші вимоги аудиту. Він дає збій там, де заголовкам Forwarded сліпо довіряють або топології проксі не документовані (кілька рівнів Ingress, Cloud-LB плюс nginx плюс Sidecar). У таких випадках Client-IP та схема швидко стають невизначеними.

Чітка межа: якщо вам потрібні складні правила довіри (CIDR, IPv6-мережі, динамічні IP-адреси балансувальника (LB)), слід розширити перевірку довіри (реальний розбір IP, мережеві маски) або спроектувати інфраструктуру так, щоб тільки визначений проксі міг досягати порту Delphi (Firewall/Security Groups). Зрештою це зазвичай більш надійне операційне рішення.

Висновок: експлуатація Reverse Proxy з nginx і Delphi вимагає «правильної обробки Forwarded»

Reverse Proxy з nginx є хорошим стандартним компонентом для Delphi-REST-серверів — але лише коректна обробка Forwarded та X-Forwarded-* робить налаштування стабільним в експлуатації. Суть проста: приймати заголовки лише від довірених проксі, послідовно виводити Client-IP/Scheme/Host і використовувати цю основу в редиректах, логуванні та перевірках безпеки. Наведеним вище снипетом ви отримуєте чисту, сумісну з legacy основу, яку можна інтегрувати в WebBroker, Horse або власні HTTP-сервери.

Якщо ви консолідуєте існуюче Delphi-бекенд за nginx або хочете модернізувати його в бік Delphi REST-API та REST-серверів з чіткою операційною лінією, технічне рев’ю ланцюжка проксі та аналізу заголовків часто є найшвидшим важелем. Зв’яжіться з Net-Base для короткої технічної оцінки.

У профільному середовищі Nginx Reverse Proxy та заголовки Forwarded також відіграють важливу роль, коли інтеграції, потоки даних і подальший розвиток повинні працювати узгоджено.

Обговорити проєкт або модернізацію з Net-Base.

Поділитися дописом

Поділитися цим дописом безпосередньо

LinkedIn, X, XING, Facebook, WhatsApp та електронна пошта доступні негайно. Для Instagram ми готуємо посилання та короткий текст безпосередньо.

Електронна пошта

Instagram відкривається в новій вкладці. Посилання та короткий текст попередньо копіюються у буфер обміну.