Перейти к основному содержимому

Сохранение IP-адреса клиента с при развертывании приложения за балансировщиком нагрузки

· 8 мин. чтения

Когда балансировщик нагрузки или прокси-сервер не могут сохранить исходный IP-адрес клиента, он может переписать IP-адрес или использовать свой собственный IP-адрес для целей маршрутизации. В этом сценарии широко используются обычные методы, такие как вставка исходного IP-адреса в заголовки запросов (например, X-Forwarded-For) или использование протокола прокси, чтобы гарантировать, что серверные службы по-прежнему имеют доступ к этой информации. Application Load Balancer работает на 7 уровне и используют HTTP-заголовок X-Forwarded-For для передачи IP-адреса клиента серверу. Но что делать если вы работаете на уровне 3 и не можете использовать HTTP-заголовки? В этом случае нужно учитывать, что Network Load Balancer (NLB) и Application Load Balancer (ALB) по-разному обрабатывают L3 трафик. Об этих различиях и пойдет речь в этой статье.

В качестве примера мы рассмотрим nginx. Мы настроим его на прием запросов с NLB и ALB в L3 и L7 вариантах. Все примеры развертывания описаны в Terraform рецепте и вы сможете легко самостоятельно проверить их.

DNAT в Network Load Balancer

Network Load Balancer (NLB) — это балансировщик нагрузки на уровне 3, который работает на уровне TCP/UDP. NLB не терминирует соединения, а просто перенаправляет их на целевые группы. Network Load Balancer изменяет IP-адрес назначения пакетов (dst) на IP-адрес целевой группы. Адрес отправителя (src) остается неизменным. Это означает, что серверы в целевой группе видят IP-адрес клиента, а не IP-адрес NLB. Такую схему называют DNAT (Destination Network Address Translation).

server {
listen 8080;
server_name demo;

location / {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

В этом конфиге nginx я хотел бы обратить ваше внимание на две строки:

  • на 2-й строке мы указываем порт 8080 без каких либо дополнительных директив;
  • на 7-й строке мы добавляем заголовок X-Forwarded-For с помощью директивы proxy_set_header. Значение мы берем из переменной $proxy_add_x_forwarded_for. Nginx берет значение из заголовка из переменной $remote_addr.

Теперь давайте при помощи Wireshark посмотрим на пакеты, как из видят клиент и сервер.

sudo tshark -i any -Y "(ip.src == 84.201.147.207 or ip.dst == 84.201.147.207) and tcp.port == 8080"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'any'
536 5.269934183 10.0.1.10 → 84.201.147.207 TCP 76 51742 → 8080 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=2755740855 TSecr=0 WS=128
538 5.270423287 84.201.147.207 → 10.0.1.10 TCP 76 8080 → 51742 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=1499863856 TSecr=2755740855 WS=128
539 5.270444869 10.0.1.10 → 84.201.147.207 TCP 68 51742 → 8080 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=2755740855 TSecr=1499863856
541 5.270686454 10.0.1.10 → 84.201.147.207 HTTP 162 GET /dump/request HTTP/1.1
543 5.270956665 84.201.147.207 → 10.0.1.10 TCP 68 8080 → 51742 [ACK] Seq=1 Ack=95 Win=65152 Len=0 TSval=1499863857 TSecr=2755740856
544 5.272402897 84.201.147.207 → 10.0.1.10 HTTP 536 HTTP/1.1 200 OK (text/plain)
545 5.272427080 10.0.1.10 → 84.201.147.207 TCP 68 51742 → 8080 [ACK] Seq=95 Ack=469 Win=64128 Len=0 TSval=2755740857 TSecr=1499863858
547 5.272843872 10.0.1.10 → 84.201.147.207 TCP 68 51742 → 8080 [FIN, ACK] Seq=95 Ack=469 Win=64128 Len=0 TSval=2755740858 TSecr=1499863858
550 5.273167331 84.201.147.207 → 10.0.1.10 TCP 68 8080 → 51742 [FIN, ACK] Seq=469 Ack=96 Win=65152 Len=0 TSval=1499863859 TSecr=2755740858
551 5.273183410 10.0.1.10 → 84.201.147.207 TCP 68 51742 → 8080 [ACK] Seq=96 Ack=470 Win=64128 Len=0 TSval=2755740858 TSecr=1499863859

Так как я отправляю запрос с VM в Yandex Cloud, где публичный IP-адрес не назначается непосредственно на интерфейс VM, то вместо публичного IP-адреса VM (158.160.42.192) в дампе с клиента виден внутренний IP-адрес (10.0.1.10).

sudo tshark -i any -Y "(ip.src == 158.160.42.192 or ip.dst == 158.160.42.192) and tcp.port == 8080"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'any'
337 16.850751189 158.160.42.192 → 10.0.1.23 TCP 76 51742 → 8080 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=2755740855 TSecr=0 WS=128
338 16.850792241 10.0.1.23 → 158.160.42.192 TCP 76 8080 → 51742 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=1499863856 TSecr=2755740855 WS=128
339 16.851148142 158.160.42.192 → 10.0.1.23 TCP 68 51742 → 8080 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=2755740855 TSecr=1499863856
340 16.851399693 158.160.42.192 → 10.0.1.23 HTTP 162 GET /dump/request HTTP/1.1
341 16.851422732 10.0.1.23 → 158.160.42.192 TCP 68 8080 → 51742 [ACK] Seq=1 Ack=95 Win=65152 Len=0 TSval=1499863857 TSecr=2755740856
352 16.852646265 10.0.1.23 → 158.160.42.192 HTTP 536 HTTP/1.1 200 OK (text/plain)
353 16.853137008 158.160.42.192 → 10.0.1.23 TCP 68 51742 → 8080 [ACK] Seq=95 Ack=469 Win=64128 Len=0 TSval=2755740857 TSecr=1499863858
354 16.853552523 158.160.42.192 → 10.0.1.23 TCP 68 51742 → 8080 [FIN, ACK] Seq=95 Ack=469 Win=64128 Len=0 TSval=2755740858 TSecr=1499863858
355 16.853591638 10.0.1.23 → 158.160.42.192 TCP 68 8080 → 51742 [FIN, ACK] Seq=469 Ack=96 Win=65152 Len=0 TSval=1499863859 TSecr=2755740858
356 16.853883528 158.160.42.192 → 10.0.1.23 TCP 68 51742 → 8080 [ACK] Seq=96 Ack=470 Win=64128 Len=0 TSval=2755740858 TSecr=1499863859

При доставке в обратном направлении пакеты проходят через Network Load Balancer, который изменяет IP-адрес источника пакетов (src) на IP-адрес NLB. Адрес назначения (dst) остается неизменным. Таким образом, клиент не видит какой из серверов в целевой группе обработал запрос.

Application Load Balancer на уровне 7

Для того чтобы настроить nginx на прием запросов с Application Load Balancer (ALB), можно использовать следующий конфиг:

server {
listen 8080;
server_name demo;

location / {
proxy_pass http://127.0.0.1:8000/;
}
}

Он мало чем отличается от конфига для NLB. Единственное, что мы не добавляем заголовок X-Forwarded-For, так как ALB сам добавляет его.

Если мы снимем дамп пакетов с клиента при использовании ALB, то ничего принципиально нового не увидим. Дело в том NLB является частью ALB и именно он первым получает пакеты от клиента.

Дальше пакеты передаются на ноды ALB, где они обрабатываются. На этих нодах ужу Envoy. Envoy — это прокси-сервер, который обрабатывает HTTP-запросы и передает их на сервера в целевой группе. Все это происходит на уровне 7.

  330 2.349194968    10.0.1.15 → 10.0.1.24    TCP 76 41656 → 8080 [SYN] Seq=0 Win=42340 Len=0 MSS=1460 SACK_PERM TSval=3710003547 TSecr=0 WS=256
331 2.349213445 10.0.1.24 → 10.0.1.15 TCP 76 8080 → 41656 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=1992268027 TSecr=3710003547 WS=128
332 2.349373980 10.0.1.15 → 10.0.1.24 TCP 68 41656 → 8080 [ACK] Seq=1 Ack=1 Win=42496 Len=0 TSval=3710003547 TSecr=1992268027
333 2.349473662 10.0.1.15 → 10.0.1.24 HTTP 309 GET /dump/request HTTP/1.1
334 2.349485446 10.0.1.24 → 10.0.1.15 TCP 68 8080 → 41656 [ACK] Seq=1 Ack=242 Win=65024 Len=0 TSval=1992268027 TSecr=3710003547
345 2.350110574 10.0.1.24 → 10.0.1.15 HTTP 573 HTTP/1.1 200 OK (text/plain)
346 2.350301857 10.0.1.15 → 10.0.1.24 TCP 68 41656 → 8080 [ACK] Seq=242 Ack=506 Win=42496 Len=0 TSval=3710003548 TSecr=1992268028

Envoy видит IP-адрес клиента в src и передает его в заголовке X-Forwarded-For на сервер. Таким образом, сервер видит IP-адрес клиента.

Так как Envoy не просто перенаправляет пакеты, а устанавливает новое соединение, то серверу же смотреть IP-адрес бесполезно, так как он будет видеть IP-адрес Envoy.

Application Load Balancer на уровне 3

Application Load Balancer кроме уровня 7 поддерживает уровень 3. Это означает, что ALB может работать с TCP-соединениями и передавать их на сервера в целевой группе. То есть в таком случае ALB не будет иметь доступа к HTTP-заголовкам и не сможет передавать IP-адрес клиента в X-Forwarded-For. Возможно это вообще не HTTP-трафик, а какой-то другой протокол. Как в таком случае передать IP-адрес клиента на сервер? Ведь, как и в случае с балансировщиком на уровне 7, соединение до сервера проходит через Envoy. А значит сервер видит IP-адрес Envoy. Именно для таких случаев и существует протокол Proxy.

Как работает Proxy protocol

Proxy protocol добавляет заголовок в начало потока TCP. Когда клиент устанавливает TCP-соединение с сервером, выполняется трехстороннее рукопожатие (a three-way handshake, SYNSYN/ACKACK), за которым следует поток данных. Заголовок Proxy протокола v1 кодирует сведения о соединении клиента и добавляется в начало потока данных, сразу после трехстороннего рукопожатия. Этот заголовок содержит информацию о версии протокола, адресах и портах клиента и сервера, а также о типе соединения. После того как заголовок Proxy протокола добавлен к потоку данных, сервер может извлечь сведения о клиенте и использовать их для маршрутизации, логирования и других целей.

Заголовок Proxy протокола в формате v1 передается в человекочитаемом виде и содержит следующие сведения о соединении:

  • Заголовок — Заголовок протокола Proxy (5 ascii символов PROXY)
  • INET протокол — Протокол сетевого уровня (TCP4, TCP6, UNKNOWN)
  • IP-адрес клиента — IP-адрес клиента
  • IP-адрес сервера — IP-адрес сервера
  • Порт клиента — Порт клиента
  • Порт сервера — Порт сервера

Эти сведения кодируются в заголовке протокола Proxy v1 и используются прокси-сервером или балансировщиком нагрузки для передачи сведений о соединении клиента на сервер. Это позволяет серверу видеть данные об оригинальном клиентском соединении.

Записав пакеты на сервере и открыв их в Wireshark, мы увидим этот пакет.

примечание

Перед тем как включить протокол Proxy для целевой группы, убедитесь, что ваши приложения ожидают и могут разбирать заголовок протокола Proxy v1, иначе они не смогут обработать его.

Чтобы настроить nginx на корректное чтение заголовка Proxy протокола, нужно использовать вот такой конфиг:

server {
listen 8080 proxy_protocol;
server_name demo;

location / {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
}
}
  • На 2-й строке мы указываем порт 8080 и добавляем директиву proxy_protocol. Это означает, что nginx ожидает, что первым пакетом будет заголовок протокола Proxy.
  • На 7-й строке мы добавляем заголовок X-Forwarded-For с помощью директивы proxy_set_header. Значение мы берем из переменной $proxy_protocol_addr. Подробнее о переменной можно почитать в документации.