Net-Base Журнал

23.05.2026

Обратный прокси с nginx и Delphi: корректная обработка заголовка Forwarded, реальный IP клиента и надежные базовые URL

Если серверы Delphi-REST работают за nginx, часто некорректно определяются IP клиента, распознавание HTTPS и абсолютные URL. Этот фрагмент исходного кода демонстрирует надёжную обработку заголовков Forwarded/X-Forwarded (включая список доверенных прокси), типичные настройки nginx и указания по отладке для эксплуатации.

23.05.2026

Обратный прокси с nginx и Delphi является на практике чаще не «nice to have», а аккуратным разграничением между краем сети и приложением: терминация TLS (HTTPS‑offloading), централизованные правила для заголовков/CORS, ограничение частоты запросов, единые логи, Blue/Green‑развертывания или просто хостинг нескольких сервисов под одним доменом. То, что часто недооценивают: как только nginx «сидит» перед сервисом, сервер Delphi видит только IP прокси, зачастую только «http» вместо «https» и генерирует неверные абсолютные ссылки (редиректы, callback‑URL, URL сервера OpenAPI). Именно эти три пункта впоследствии добавляют время на отладку в эксплуатации.

Этот фрагмент исходного кода демонстрирует надёжную схему, как в Delphi корректно разбирать заголовки Forwarded и X-Forwarded-* — включая список доверенных прокси (важно против подделки заголовков) и консистентный базовый URL запроса. Приведены практичные конфигурации nginx и указания по пограничным случаям, таким как WebSockets, большие загрузки и таймауты.

Почему настройки обратного прокси «сбивают с толку» серверы Delphi

nginx как обратный прокси обычно общается с сервисом 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‑конфигурация: минимально полезные proxy‑заголовки

Надёжная отправная точка (HTTP/1.1, Keep-Alive, Upgrade для WebSockets) выглядит так. Фрагмент сознательно сдержан; в зависимости от окружения вы добавите HSTS, Rate-Limits и 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;
  }
}

Цель: Приложению надежно передаются Host, IP клиента и схема. Ограничение: $proxy_add_x_forwarded_for добавляет текущий IP прокси в возможную существующую цепочку; это полезно для настроек с несколькими прокси, но делает корректный разбор на стороне Delphi тем более важным. Подводный камень: Если вы в nginx не устанавливаете заголовок Host, Delphi возможно увидит только upstream-хост (127.0.0.1), что нарушит перенаправления и проверки Origin.

Delphi фрагмент исходного кода: надежный разбор Forwarded/X-Forwarded (с списком доверенных прокси)

Следующий код намеренно нейтрален к фреймворку: он работает с минимальным интерфейсом (Header + RemoteIP) и может быть адаптирован в WebBroker, RAD Server или Horse. Ключевые моменты:

  • Приоритет: RFC Forwarded (если присутствует) перед X-Forwarded-*.
  • Доверие: учитывать заголовки Forwarded только если прямой пир (RemoteIP) является известным прокси.
  • Разбор: учитывать IPv6, кавычки, порты и цепочки в X-Forwarded-For.
  • Выход: базовый URL, который можно использовать для абсолютных ссылок, перенаправлений или 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: 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. Это сделает возможным обход аудита, лимитов скорости или эндпоинтов «только для внутренней сети». Доверяйте Forwarded-Header’ам только если прямой пёр (RemoteIP) — это прокси, которым вы управляете (например, 127.0.0.1, IP балансировщика нагрузки, Kubernetes-Ingress).

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

Интеграция в WebBroker/Horse/RAD Server: где код «пришвартовывается»

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

Практически проверено: при старте сервиса создайте из конфигурации (INI/ENV) TTrustedProxyList, например «127.0.0.1» для локальных nginx-настроек или IP вашего балансировщика нагрузки. Затем для каждого запроса вызывайте 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, стриминг и большие запросы

WebSockets за nginx

Для WebSockets nginx требует корректных заголовков Upgrade и Connection. Дополнительно бэкенд не должен закрывать соединение «слишком рано». На стороне Delphi важно, чтобы ваша WebSocket-компонента (или SSE/Streaming-эндпоинт) корректно работала с reverse-прокси и имела аккуратно реализованные heartbeats/keep-alives.

Большие загрузки и 413-ошибки

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

HTTPS-Offloading und „Secure Cookies“

Если ваш Delphi-сервис устанавливает сессионные cookies, они при внешнем HTTPS, как правило, должны быть помечены как Secure. Делает ли это ваше приложение, часто зависит от того, «знает» ли оно, что исходный запрос был по HTTPS. Именно здесь помогает последовательная обработка X-Forwarded-Proto/Forwarded.

Когда эти усилия оправданы — и где они могут дать сбой

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

Чёткая граница: если вам нужны сложные правила доверия (CIDR, IPv6‑сети, динамические IP балансировщиков нагрузки), стоит расширить проверку доверия (реальный разбор IP, сетевые маски) или спроектировать инфраструктуру так, чтобы доступ к порту Delphi имел только определённый прокси (межсетевой экран/ Security Groups). В конце концов это обычно более надёжное оперативное решение.

Вывод: корректная эксплуатация обратного прокси на nginx и Delphi означает «правильную обработку Forwarded»

Обратный прокси на nginx является для Delphi-REST-Server хорошим стандартным элементом, — но лишь корректная обработка Forwarded и X-Forwarded-* делает настройку стабильной в эксплуатации. Суть проста: принимать заголовки только от доверенных прокси, последовательно выводить Client-IP/Scheme/Host и применять эту базу в редиректах, логировании и проверках безопасности. С приведённым выше сниппетом у вас есть чистая, совместимая с legacy основа, которую можно интегрировать в WebBroker, Horse или собственные HTTP‑серверы.

Если вы консолидируете существующий Delphi-бекенд за nginx или хотите модернизировать в сторону Delphi REST-API и REST-Server с ясной операционной линией, технический ревью цепочки прокси и обработки заголовков часто является самым быстрым рычагом. Свяжитесь с Net-Base для короткой технической оценки.

В профессиональном контексте обратный прокси Nginx и заголовки Forwarded также играют важную роль, когда интеграции, потоки данных и дальнейшая разработка должны работать согласованно.

Обсудить проект или работу по модернизации с Net-Base.

Поделиться записью

Поделиться этой записью напрямую

LinkedIn, X, XING, Facebook, WhatsApp и E-Mail доступны сразу. Для Instagram мы сразу подготовим ссылку и краткий текст.

Электронная почта

Instagram открывается в новой вкладке. Ссылка и короткий текст предварительно копируются в буфер обмена.