2025, Dec 26 03:01
Почему getpeername в rootless Podman возвращает другой IP
Почему getpeername в rootless Podman с мостовой сетью возвращает иной IP: влияние NAT/hairpin. Пример на Python/C и варианты: pasta или slirp4netns.
Диагностика неожиданных сетевых проявлений в контейнерах часто начинается с привычного системного вызова. В нашем случае getpeername() внутри rootless‑контейнера Podman в мостовой сети возвращает другой адрес пира, чем при запуске той же нагрузки на сетевом стеке хоста. На первый взгляд это сбивает с толку, ведь код, вызывающий getpeername(), идентичен. Ключевой момент в том, что вызов возвращает то, что ядро записало для сокета, а в сценарии с rootless‑мостом эта запись может отличаться от ожидаемой.
Воспроизведение симптома на минимальном сервере
Ниже — крошечный TCP‑сервер: он принимает соединение и выводит адрес пира так, как его видит процесс в контейнере. Запустите его внутри rootless‑контейнера Podman, настроенного на мостовую сеть, затем подключитесь с клиента и сравните вывод с тем же тестом, выполненным с использованием сети хоста.
import socket
HOST = "0.0.0.0"
PORT = 8080
srv_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv_sock.bind((HOST, PORT))
srv_sock.listen(1)
print("listening on", (HOST, PORT))
conn_sock, addr_info = srv_sock.accept()
print("accepted from:", addr_info)
print("getpeername():", conn_sock.getpeername())
conn_sock.close()
srv_sock.close()
Если удобнее на C — идея та же: принять соединение и вызвать getpeername() на принятом сокете.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>
int main(void) {
int ls = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in bind_addr; memset(&bind_addr, 0, sizeof(bind_addr));
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(8080);
int opt = 1;
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bind(ls, (struct sockaddr*)&bind_addr, sizeof(bind_addr));
listen(ls, 1);
struct sockaddr_in src; socklen_t slen = sizeof(src);
int cs = accept(ls, (struct sockaddr*)&src, &slen);
char buf[64];
inet_ntop(AF_INET, &src.sin_addr, buf, sizeof(buf));
printf("accepted from: %s:%d\n", buf, ntohs(src.sin_port));
struct sockaddr_in peer; socklen_t plen = sizeof(peer);
getpeername(cs, (struct sockaddr*)&peer, &plen);
inet_ntop(AF_INET, &peer.sin_addr, buf, sizeof(buf));
printf("getpeername(): %s:%d\n", buf, ntohs(peer.sin_port));
close(cs);
close(ls);
return 0;
}
Что на самом деле происходит
Причина в том, как в Podman реализована rootless‑сеть. API getpeername() делает свою работу: возвращает адрес, сохранённый ядром для сокета. Несовпадение появляется из‑за того, что при rootless‑мосте сетевой стек контейнера изначально не видит исходный IP‑адрес. По духу это похоже на эффект hairpinning при NAT, с которым сталкиваются при пробросе портов на домашних роутерах. Механика обсуждается в сообществах, например в этом issue и этой дискуссии.
Иными словами, API честно отражает состояние сокета; запись меняет окружающий слой rootless‑сети.
Практические варианты обхода
Есть режимы, которые меняют поведение rootless‑сети. В связанной дискуссии рекомендации сформулированы прямо:
you can set
network_mode: pastaornetwork_mode: port_handler=slirp4netnsbut this is incompatible with named (user-defined) networks, so it is a either or situation for now.Fixing this is not trivial at all [...]
Если вы управляете конфигурацией контейнера, смена сетевого режима может привести отчёт getpeername() в соответствие с ожиданиями вашей среды. Ниже минимальная заготовка конфигурации, иллюстрирующая подход:
services:
app:
network_mode: pasta
Либо так:
services:
app:
network_mode: port_handler=slirp4netns
Помните, что эти режимы, как отмечено выше, несовместимы с именованными пользовательскими сетями — это компромисс.
Зачем это важно для диагностики и наблюдаемости
Метаданные на уровне сокетов лежат в основе контроля доступа, лимитирования, аудита и атрибуции запросов. Когда rootless‑мост скрывает исходный IP, серверный код, полагающийся на getpeername() для идентификации пира, добросовестно зафиксирует уже трансформированный адрес. С точки зрения ядра это корректно, но операторов, ожидающих IP конечных клиентов, это может удивить.
Итоги и рекомендации
Если getpeername() в rootless‑контейнере Podman с мостовой сетью возвращает неожиданные адреса, воспринимайте это как артефакт механизма rootless‑сети, а не как ошибку API. Проверьте ожидания, сравнив с сетью хоста, и при необходимости рассмотрите переход на network_mode: pasta или network_mode: port_handler=slirp4netns, понимая, что этот выбор исключает именованные пользовательские сети. Используйте это знание, чтобы корректно настроить логирование и ACL для нагрузок, которым важна видимость IP‑адресов пиров.