
Комментарии 7
А в чем минус приложению работать на ipv4 как обычно, а проброс адресов выполнять на nginx:
server {
server_tokens off;
listen 443 ssl;
listen [2001:111:11:111::1]:443 ssl;
server_name test.ru;
ssl_certificate fullchain.pem;
ssl_certificate_key privkey.pem;
location @backend {
proxy_pass http://127.0.0.1:2000;
}
proxy_redirect off;
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;
}Таким образом абстрагирован прикладной уровень от сетевого, там nginx разберется что и куда.
Минуса по сути нет, вариант рабочий — это как раз одна из тех развязок, о которых речь в статье («развести слушателей»). Вы вынесли разбор обоих стеков на nginx, а бэкенд общается с ним по чистому IPv4 через 127.0.0.1 и mapped-адрес не увидит вовсе. Хороший подход, мне нравится.
Проблема при этом никуда не девается, просто стягивается в одну точку — в сам nginx. Пока listen раздельные, как у вас (443 и конкретный [2001:…]), всё чисто. Но стоит где-то появиться listen [::]:443 без ipv6only=on — и в логах самого nginx $remote_addr поедет в форме ::ffff:…, теперь уже на уровне прокси.
И второе: реальный адрес клиента теперь приезжает в приложение не из сокета, а из X-Forwarded-For, который вы прокидываете. Если перед nginx окажется ещё один прокси на дуал-стек сокете, mapped-форма может прилететь и в этот заголовок — а его обычно разбирают руками. Так что приём отличный, просто точка, где держать нормализацию адреса, смещается с сокета приложения на границу прокси.
Я почему указывал `listen [2001:111:11:111::1]:443 ssl;`, потому что выше стоит ipv6 firewall, который валидирует ipv6 трафик на пограничном шлюзе. Что можно, а что нельзя, дабы ipv6 адреса внутри вверенной инфры жили спокойно.
Правильный ответ достаётся только через .ipv4_mapped, о котором нужно знать заранее
Можно не знать о нем. Просто адреса нужно разбирать по версии протокола в коде (библиотека это умеет), а не считать все как v4.
Новые приложения вообще лучше писать так, чтобы они понимали v6, а v4 хранить как mapped.
Хороший разбор, взгляд зацепился:
Код отрабатывает штатно, тест на «адрес не из сети — отказать» проходит, ревью проходит тоже.
Есть небольшой опыт в тестировании, это классика: проверку вайтлиста тестируют как чистую функцию - подали строку "192.0.2.5", получили allow, подали "10.0.0.1" - deny, все зеленое. А в проде адрес приходит не строкой из теста, а из сокета, и баг живет ровно на этом стыке. Юнит-тестом его не поймать в принципе, хоть 100% покрытия сделай.
Ловится только интеграционным тестом через реальный сокет: поднимаем сервис на "::", подключаемся по 127.0.0.1 и проверяем, что вайтлист со 127.0.0.1 пропускает. Один такой тест в CI - и вся эта история падает на пулл-реквесте с переездом на dual-stack, а не на заявке от клиента. Но так почти никто не делает: проверка адреса выглядит слишком простой, чтобы гонять ее не юнитом.
А разве описанный вами вариант не является просто случаем "некорректный юнит-тест"? Если адрес приходит строкой, в нем вообще что-угодно может быть.
Отчасти да - если знать про mapped-адреса, то "::ffff:192.0.2.5" просто добавляется в юнит-кейсы, и после инцидента именно так и делается, это правильный регрессионный тест. Вопрос в том, откуда этот кейс возьмется в списке до инцидента.
Юнит-тест проверяет функцию против нашего представления о входах - что придет строка с чем угодно - да, но "что угодно" в тестах всегда конечный список, который мы сами и придумали. Если про mapped-форму не знаешь, ее в списке не будет, и тест честно зеленый. А интеграционный через сокет проверяет само представление: он приносит в проверку тот вход, который реально отдает ядро, без нашего участия. Баг ведь не в функции сравнения - она корректно сравнивает то, что дали. Баг на стыке "что отдает сокет" и "что ожидает проверка", а стыки юнитами не проверяются по определению, на то они и юниты.
В логе адрес ::ffff:192.0.2.5, в белом списке 192.0.2.5. Это один адрес, и он не совпадает