Сохранение IP-адреса клиента с при развертывании приложения за балансировщиком нагрузки
Когда балансировщик нагрузки или прокси-сервер не могут сохранить исходный 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,
SYN
→SYN/ACK
→ACK
), за которым следует поток данных.
Заголовок 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
. Подробнее о переменной можно почитать в документации.