Зачем вообще говорить про NodePort, IP клиента и ExternalTrafficPolicy
Типичная задача: вы публикуете сервис в Kubernetes наружу через Service типа NodePort или через LoadBalancer (который в большинстве реализаций всё равно опирается на NodePort на нодах). Дальше внезапно выясняется, что приложение или reverse proxy (Nginx/Envoy/Traefik) видит не IP клиента, а адрес ноды или балансировщика. Логи становятся бесполезными, rate limit «на клиента» не работает, аудит усложняется.
Виновата не одна «галочка», а связка: режим kube-proxy (iptables или IPVS), необходимость обеспечить корректный обратный маршрут и настройка externalTrafficPolicy. Ниже разложим по полочкам, что именно происходит на уровне сети и как получить сохранение IP клиента без неожиданных побочных эффектов в балансировке.
Важно заранее разделить два требования: «сохранить TCP source IP в поде» и «получить корректный client IP на уровне HTTP». Это разные задачи и решаются разными способами.
Как NodePort принимает трафик: короткая модель в голове
NodePort открывает порт на каждой ноде кластера (обычно диапазон 30000–32767) и заставляет kube-proxy перенаправлять трафик на один из endpoints (подов), подходящих под селектор сервиса.
Ключевой нюанс: клиент приходит на IP ноды, а выбранный endpoint может жить на другой ноде. И тут появляется вопрос маршрутизации: что будет, если пакет «прилетел» на ноду A, а под находится на ноде B?
Почему появляется SNAT и куда пропадает реальный IP
Если нода A принимает соединение и пересылает его на под на ноде B, то без подмены адресов обратный ответ в ряде сетевых схем может уйти к клиенту напрямую (мимо ноды A). Для TCP это часто означает разрыв соединения из-за асимметрии маршрута.
Чтобы гарантировать симметричный путь (вход через A и выход через A), kube-proxy часто включает SNAT (обычно через MASQUERADE) и подменяет source IP на адрес ноды. В итоге под видит source как «адрес ноды», а не реального клиента.
Пока сервис может отправить внешний трафик на удалённые endpoints (на других нодах), Kubernetes вынужден выбирать компромисс между «работает при любой раскладке подов» и «сохраняем IP клиента». Компромисс задаёт
externalTrafficPolicy.
Если вы разворачиваете кластер на собственных серверах или в облаке без managed-LB, часто это делают на VDS, и там особенно важно заранее продумать вход в кластер: какой IP должен видеть ingress/приложение и какой слой будет делать балансировку.

externalTrafficPolicy: Cluster vs Local — что меняется
У Service есть поле externalTrafficPolicy с двумя режимами:
Cluster(по умолчанию): трафик может быть отправлен на любой endpoint по всему кластеру. Балансировка равномернее, но часто включается SNAT и теряется реальный IP.Local: нода принимает внешний трафик только если на ней есть локальные endpoints этого сервиса. Тогда «перепрыгивания» на другую ноду не происходит, SNAT обычно не нужен, и IP клиента сохраняется на уровне TCP.
Практический эффект externalTrafficPolicy=Local
Если включить externalTrafficPolicy: Local, вы обычно получаете:
- поды видят реальный IP клиента (если его не «ломает» внешний балансировщик);
- часть нод перестаёт быть точкой входа, если на них нет endpoints;
- внешний балансировщик должен уметь отправлять трафик только на «здоровые» ноды (где есть локальные endpoints).
Пример манифеста NodePort с externalTrafficPolicy=Local
Минимальный пример сервиса, где мы явно хотим сохранить IP клиента:
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
externalTrafficPolicy: Local
selector:
app: web
ports:
- name: http
port: 80
targetPort: 8080
nodePort: 30080
Проверка «на пальцах»: если у вас 3 ноды, а поды app=web крутятся только на одной, то только эта нода будет корректной точкой входа на 30080. Остальные ноды не должны принимать этот трафик, иначе придётся проксировать на другую ноду и вы снова упрётесь в SNAT или в проблемы с возвратным маршрутом.
HealthCheckNodePort: зачем он нужен и почему без него бывает плохо
Когда вы включаете externalTrafficPolicy: Local на Service типа LoadBalancer, Kubernetes обычно выделяет дополнительный порт healthCheckNodePort. Идея простая: внешний балансировщик проверяет этот порт на каждой ноде и понимает, есть ли на ней локальные endpoints.
Без такой проверки балансировщик может продолжать слать трафик на ноды, где нет подов сервиса. Результат выглядит как «рандомные» таймауты, 503 или плавающие ошибки по сети.
Если у вас «железный» L4-балансировщик перед NodePort, задачу health check’ов он тоже должен решать: либо через отдельные проверки/скрипты, либо через интеграцию с Kubernetes-контроллером, иначе externalTrafficPolicy: Local превратится в лотерею.
Как посмотреть healthCheckNodePort у сервиса
kubectl get svc web-lb -o yaml
Ищите поле healthCheckNodePort в описании сервиса. Оно появляется не всегда: зависит от типа сервиса и контроллера, который реализует LoadBalancer.
kube-proxy: iptables vs IPVS и как это влияет на SNAT
Смысл в обоих режимах одинаковый: обеспечить виртуальный сервис и балансировку на endpoints. Но диагностика отличается:
- iptables: правила в цепочках NAT/Filter создают сервис и распределяют трафик. SNAT обычно виден как
MASQUERADEв таблицеnat. - IPVS: ядро делает L4-балансировку через IPVS-сервисы, а iptables остаётся для части обвязки (включая некоторые правила NAT).
Если цель — сохранить TCP source IP, главное не «iptables или IPVS», а разрешаете ли вы пересылку на удалённые endpoints. Как только разрешаете «любой под в кластере» для внешнего трафика, вероятность SNAT резко растёт.
Быстрая диагностика: есть ли SNAT на ноде
На ноде (нужны права root) можно посмотреть правила NAT:
iptables -t nat -S | grep -E 'KUBE|MASQUERADE'
В IPVS-режиме дополнительно полезно посмотреть таблицы IPVS:
ipvsadm -Ln
Практическая цель диагностики: понять, где включается MASQUERADE, и связано ли это именно с вашим сервисом/входящим трафиком.
Почему «реальный IP» может теряться даже с Local
externalTrafficPolicy: Local решает проблему SNAT на уровне ноды Kubernetes, но не отменяет того, что перед кластером может стоять ещё один слой: облачный балансировщик, аппаратный L4, reverse proxy, CDN. Каждый из них может терять или заменять source IP.
- L7 proxy (Ingress-контроллер как reverse proxy) завершает TCP/HTTP и подключается к вашему сервису сам. Тогда backend увидит IP ingress-пода, а реальный адрес будет только в HTTP-заголовках вроде
X-Forwarded-ForилиX-Real-IP. - DNAT/SNAT на внешнем балансировщике: часть устройств/провайдеров по умолчанию NAT’ит клиентов.
- PROXY protocol: иногда решает сохранение адреса на L4, но требует поддержки на стороне приёмника (ingress, сервис, приложение).
Отсюда рабочая формулировка задачи: «где именно мне нужен client IP — в backend-поде на уровне TCP, в логах ingress или только в HTTP-логике приложения». И уже под это выбирается схема.
Если у вас входной слой построен на TCP/UDP-прокси, полезно держать под рукой материал про балансировку на уровне stream-модуля: балансировка TCP/UDP в Nginx (stream).
MetalLB и Service LoadBalancer: как это связано с NodePort и IP клиента
В онпреме и на собственных серверах часто используют MetalLB, чтобы получить Service типа LoadBalancer без облачного провайдера. Важно помнить: многие реализации LoadBalancer в Kubernetes всё равно используют NodePort как «точку входа» на нодах, а уже затем трафик попадает в сервис.
Практически это означает:
- В L2-режиме (ARP/NDP) MetalLB «размещает» внешний IP на одной из нод и трафик приходит туда. Дальше решает
externalTrafficPolicy: сClusterвыше шанс SNAT, сLocalвыше шанс сохранить IP. - В BGP-режиме трафик может приходить на разные ноды по маршрутизации. С
externalTrafficPolicy: Localэто обычно сочетается хорошо, но критично, чтобы ноды без endpoints не становились точкой входа (health check и корректная реклама маршрутов).
В обоих вариантах распределение подов по нодам и понятная схема health check становятся ключевыми. Если вы не контролируете, где запущены поды, вы не контролируете, какие ноды должны принимать внешний трафик.
Паттерны, которые реально работают в проде
1) Local + распределение подов по нодам (anti-affinity/spread)
Самый прямой способ избежать «дыр» — гарантировать, что на каждой ноде, куда может прийти трафик, есть endpoint. Обычно делают так:
- увеличивают число реплик;
- используют
podAntiAffinity, чтобы реплики не собирались на одной ноде; - добавляют
topologySpreadConstraintsдля более ровного распределения.
Так вы получаете и сохранение IP клиента, и предсказуемое распределение нагрузки.
2) Local + «входные» ноды (nodeSelector/taints)
Если не хотите распылять поды по всему кластеру, выделяют 2–3 «ingress-ноды»:
- на них запускают только входные компоненты (ingress/controller или gateway);
- внешний балансировщик или MetalLB направляет трафик только туда;
externalTrafficPolicy: Localвключают, чтобы source IP не терялся на последнем прыжке.
Плюс: проще управлять. Минус: это архитектурное решение, которое требует дисциплины в планировании и отказоустойчивости.
3) Не бороться за TCP source IP, а работать с X-Forwarded-For
Иногда требование «видеть IP клиента» относится только к логам/аналитике на уровне HTTP. Тогда проще принять, что ingress завершает соединение, а IP передаётся заголовками. Но обязательно:
- ограничить, кто может присылать эти заголовки (иначе их подделают);
- настроить доверенные прокси на стороне Nginx/Envoy/приложения;
- чётко понимать цепочку проксирования и где именно формируется «истинный» client IP.
Для эксплуатации полезно заранее продумать, как вы будете хранить и разбирать логи, особенно если IP важен для расследований: практика по логам, ротации и алертам.

Частые симптомы и быстрые проверки
Симптом: приложение видит IP ноды вместо клиента
- Проверьте
externalTrafficPolicyу сервиса. - Убедитесь, что перед сервисом не стоит L7-прокси, который завершает соединение.
- Проверьте, есть ли endpoints на той ноде, куда приходит трафик (особенно при
Local).
Симптом: часть запросов падает/таймаутит после включения Local
- Чаще всего балансировщик продолжает слать трафик на ноды без локальных endpoints.
- Проверьте наличие и использование
healthCheckNodePort(если этоLoadBalancer). - Проверьте распределение реплик по нодам (anti-affinity/spread) или схему «входных нод».
Симптом: всё работает, но в логах ingress «не тот IP»
- Если ingress завершает HTTP, backend увидит IP ingress, а клиентский адрес будет в заголовках (если вы их настроили и доверяете источнику).
- Проверьте, что вы не смешиваете требования «TCP source IP» и «HTTP client IP».
Мини-чеклист: как добиться сохранения client IP с NodePort
Определите, где нужен IP: в backend-поде на уровне TCP, в ingress, или только в HTTP-заголовках.
Если нужен TCP source IP в поде — используйте
externalTrafficPolicy: Localи убедитесь, что трафик попадает на ноды с локальными endpoints.Обеспечьте health check на уровне внешнего балансировщика. Для
LoadBalancerпроверьтеhealthCheckNodePort.Распределите поды по нодам (anti-affinity/spread) или выделите «входные ноды» и маршрутизируйте на них.
Если есть L7-прокси — настройте доверенную передачу IP (заголовки или PROXY protocol) и защиту от подмены.
Итоги
NodePort — быстрый способ опубликовать сервис, но он часто приводит к потере IP клиента из-за SNAT в kube-proxy. Режим externalTrafficPolicy: Local помогает сохранить source IP, но требует дисциплины: трафик должен попадать на ноды с локальными endpoints, а внешний балансировщик обязан корректно отсекать «пустые» ноды проверками здоровья.
Если вы используете MetalLB или любой Service типа LoadBalancer в своём кластере, логика та же: сохранение client IP — это цепочка решений от входа в кластер до конкретного пода. При правильно собранной схеме вы получаете и стабильную доставку трафика, и корректный IP для логов, лимитов и безопасности.


