Публикуем сайт и API по IPv6: dual stack без риска для текущих пользователей

В этой статье мы разберем, как настроить уже работающий сайт и его API для приема подключений по IPv6 без потери доступности по IPv4. Также мы рассмотрим, как проверить доступность IPv6 на сервере, какие изменения потребуются в конфигурации Nginx, Apache и их связки и как добавить AAAA-запись в DNS.

Что такое IPv6 и dual stack

IPv6 – версия протокола, разработанная на смену IPv4. Основное отличие – длина адреса: 128 бит вместо 32, что дает практически неограниченное адресное пространство. Адрес записывается шестнадцатеричными группами, разделенными двоеточиями, например: 2001:db8:1a2b:3c4d::1.

Dual stack – режим, при котором сервер одновременно принимает подключения по обоим протоколам. У домена настроены две записи: A с адресом IPv4 и AAAA с адресом IPv6, веб-сервер слушает оба протокола, а клиент при подключении выбирает подходящий самостоятельно.

Протоколы работают параллельно и независимо друг от друга, поэтому доступность сайта по IPv4 не меняется. Переносить данные и править код сайта не требуется – достаточно изменений в конфигурации веб-сервера и одной записи в DNS.

Проверка доступности IPv6 на сервере

Подключите IPv6 в панели управления, в настройках нужного сервера в разделе «Облако». После этого подключитесь к серверу по SSH и посмотрите адреса на сетевом интерфейсе:

ip -6 addr show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 2001:db8:1a2b:3c4d::1/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::257:24ff:fe26:65f7/64 scope link
       valid_lft forever preferred_lft forever

Требуется адрес с пометкой scope global – это публичный адрес сервера, именно он потребуется для AAAA-записи. Адреса с пометкой scope link, начинающиеся с fe80::, являются служебными и в интернете не маршрутизируются.

Префикс /64 означает, что серверу выделена целая подсеть, а не один адрес: ее хватит на все сайты и сервисы. Для публикации сайта по IPv6 достаточно адреса, который уже назначен на интерфейс. 

Если сетевой интерфейс называется иначе, посмотрите список интерфейсов командой ip -6 addr show без указания устройства.

Проверьте связность по IPv6 – для этого подойдет любой публичный адрес, например, DNS-сервер Google:

ping -6 -c3 2001:4860:4860::8888
PING 2001:4860:4860::8888 (2001:4860:4860::8888) 56 data bytes
64 bytes from 2001:4860:4860::8888: icmp_seq=1 ttl=118 time=13.8 ms
64 bytes from 2001:4860:4860::8888: icmp_seq=2 ttl=118 time=13.4 ms
64 bytes from 2001:4860:4860::8888: icmp_seq=3 ttl=118 time=13.2 ms
--- 2001:4860:4860::8888 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 13.228/13.472/13.802/0.241 ms

Если адрес назначен, но ping не проходит, проверьте наличие маршрута по умолчанию:

ip -6 route show
2001:db8:1a2b:3c4d::/64 dev eth0 proto kernel metric 256 pref medium
fe80::/64 dev eth0 proto kernel metric 256 pref medium
default via fe80::1 dev eth0 metric 1024 onlink pref medium

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

Приоритет IPv6 и IPv4 для исходящих соединений

После подключения IPv6 сервер начинает использовать его не только для приема входящих подключений, но и для собственных запросов – обращений приложений к сторонним API, загрузки обновлений, отправки почты.

Порядок определяет системная библиотека: адреса назначения сортируются по правилам RFC 6724. В стандартной конфигурации glibc встроенные правила дают IPv6 более высокий приоритет, чем IPv4. Посмотреть фактический порядок можно командой:

getent ahosts example.ru

Например:

2001:db8:1a2b:3c4d::1 STREAM example.ru
2001:db8:1a2b:3c4d::1 DGRAM
2001:db8:1a2b:3c4d::1 RAW
81.200.119.100 STREAM
81.200.119.100 DGRAM
81.200.119.100 RAW

Адрес, стоящий первым, будет первым в списке адресов, переданном приложению. Гарантии, что приложение установит соединение именно с ним, это не дает: приложение может выбирать адрес самостоятельно или явно указывать протокол. Но если исходящий IPv6 работает нестабильно, часть запросов может завершаться ошибкой, притом что сайт по-прежнему открывается по IPv4.

Приоритет настраивается в файле /etc/gai.conf. По умолчанию он состоит из комментариев, поэтому используются встроенные значения. Если в файл добавить собственные правила precedence, они заменят встроенную таблицу приоритетов целиком. Поэтому при изменении таблицы остальные нужные значения необходимо указать в файле явно.

Отдельная таблица меток, которую показывает команда ip addrlabel list, относится к выбору адреса-источника и не определяет порядок адресов назначения.

Настройка касается только исходящих соединений сервера. По какому протоколу к сайту подключаются посетители, она не определяет.

Дальнейшие действия зависят от того, какой веб-сервер обслуживает сайт. Перейдите к соответствующему разделу: «Настройка Nginx», «Настройка Apache» или «Настройка связки Nginx и Apache».

Обратите внимание!
Фильтрация трафика для IPv4 и IPv6 настраивается отдельно: правила, ограничивающие доступ по IPv4, на подключения по IPv6 не распространяются. Если на сервере используется UFW или другой файрвол, настройте для IPv6 такие же ограничения до публикации AAAA-записи. Иначе службы, закрытые от внешнего доступа по IPv4, окажутся доступны по новому протоколу.

Настройка Nginx

Откройте конфигурационный файл сайта, он находится в каталоге /etc/nginx/sites-available/:

sudo nano /etc/nginx/sites-available/example.conf

Конфигурация сайта, работающего только по IPv4, имеет примерно следующий вид:

server {
    listen 80;
    server_name example.ru;
    root /var/www/example/public;
    index index.html;
    location /api/ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME /var/www/example/api/index.php;
        include fastcgi_params;
    }
}

Добавьте в блок server строку listen [::]:80;:

server {
    listen 80;
    listen [::]:80;
    server_name example.ru;
    root /var/www/example/public;
    index index.html;
    location /api/ {
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME /var/www/example/api/index.php;
        include fastcgi_params;
    }
}

Назначение директив:

  • listen 80 – прием подключений по IPv4 на 80 порту, эта строка остается без изменений;
  • listen [::]:80 – прием подключений по IPv6 на том же порту;
  • [::] – обозначение всех адресов IPv6 сервера, аналог 0.0.0.0 при подключении через IPv4. Квадратные скобки отделяют адрес от номера порта: в адресе IPv6 двоеточия уже используются как разделители групп.

Больше ничего менять не требуется: блоки location, обработка API и остальная логика остаются без изменений – они не зависят от протокола, по которому пришел запрос.

Если на сайте установлен SSL-сертификат, аналогичную строку нужно добавить и в блок, отвечающий за 443 порт:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.ru;
    ...
}

Пропуск этого шага – частая причина ситуации, когда по IPv6 сайт открывается по HTTP, но не открывается по HTTPS.

Проверьте конфигурацию и перезапустите Nginx:

sudo nginx -t
sudo systemctl restart nginx
Обратите внимание!
При изменении директив listen используйте restart, а не reload. Перезагрузка конфигурации не всегда пересоздает сетевые сокеты: рабочие процессы могут продолжать использовать прежние. Перезапуск занимает доли секунды, но в этот момент сайт недоступен по обоим протоколам, поэтому выполняйте его только после успешной проверки конфигурации командой nginx -t.

Проверьте, что сервер слушает оба протокола:

sudo ss -tlnp | grep :80
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=44444,fd=5),("nginx",pid=44443,fd=5))
LISTEN 0      511             [::]:80           [::]:*    users:(("nginx",pid=44444,fd=6),("nginx",pid=44443,fd=6))

Две строки – два сокета: 0.0.0.0:80 для IPv4 и [::]:80 для IPv6.

Проверьте работу сайта и API по обоим протоколам, указав путь к своему API вместо /api/:

curl -4 http://127.0.0.1/
curl -6 http://[::1]/
curl -4 http://127.0.0.1/api/
curl -6 http://[::1]/api/

Ответы, полученные по IPv4 и IPv6, должны совпадать. 

Адрес [::1] – это локальная петля IPv6, аналог 127.0.0.1. Такая проверка подтверждает, что сокет создан и приложение отвечает по обоим протоколам, но не проверяет доступность сервера извне. Доступность по публичному адресу проверяется в разделе «Настройка DNS: AAAA-запись».

Ниже приведен пример тестового сайта – гостевой книги, обслуживаемой Nginx. Приложение выводит IP-адрес, с которого осуществлялось подключение, а также протокол текущего подключения и используемый веб-сервер, а каждая запись помечается протоколом, по которому она была добавлена:

пример Nginx

Записи, добавленные по IPv4 и IPv6, попадают в один общий список – это один и тот же сайт, доступный по двум протоколам.

Настройка Apache

В Ubuntu и Debian пакет Apache собран с поддержкой IPv4-mapped адресов: директива Listen 80 без указания конкретного адреса создает сокет, принимающий подключения по обоим протоколам. Проверьте, собран ли Apache таким образом:

sudo apache2ctl -V | grep APR
Server loaded:  APR 1.7.2, APR-UTIL 1.6.3, PCRE 10.42 2022-12-11
Compiled using: APR 1.7.2, APR-UTIL 1.6.3, PCRE 10.42 2022-12-11
 -D APR_HAS_SENDFILE
 -D APR_HAS_MMAP
 -D APR_HAVE_IPV6 (IPv4-mapped addresses enabled)

Строка IPv4-mapped addresses enabled означает, что поддержка IPv6 и IPv4 доступна. Она применяется только в том случае, если в конфигурации не указан конкретный адрес для прослушивания. Поэтому следующим шагом откройте файл ports.conf:

sudo nano /etc/apache2/ports.conf

Стандартное содержимое файла имеет следующий вид:

Listen 80
<IfModule ssl_module>
	Listen 443
</IfModule>
<IfModule mod_gnutls.c>
	Listen 443
</IfModule>

Адрес в директиве Listen не указан, поэтому сокет уже принимает подключения по обоим протоколам – изменения не требуются. Убедитесь в этом:

sudo ss -tlnp -6 | grep :80
LISTEN 0      511                *:80              *:*    users:(("apache2",pid=45179,fd=4),("apache2",pid=45178,fd=4),("apache2",pid=45177,fd=4),("apache2",pid=45176,fd=4),("apache2",pid=45175,fd=4),("apache2",pid=45173,fd=4))

Адрес * означает, что сокет принимает подключения на всех адресах сервера. Это и есть сокет IPv6 с поддержкой IPv4-mapped адресов: он обслуживает оба протокола.

Проверьте работу сайта и API по обоим протоколам, указав путь к своему API вместо /api/:

curl -4 http://127.0.0.1/
curl -6 http://[::1]/
curl -4 http://127.0.0.1/api/
curl -6 http://[::1]/api/

Ответы, полученные по IPv4 и IPv6, должны совпадать. 

Пример того же тестового сайта, обслуживаемого Apache:

пример Apache

Если в директиве Listen указан адрес IPv4

Если в файле ports.conf адрес указан явно, сокет создается только для IPv4:

Listen 0.0.0.0:80
<IfModule ssl_module>
	Listen 0.0.0.0:443
</IfModule>
<IfModule mod_gnutls.c>
	Listen 443
</IfModule>

В этом случае команда sudo ss -tlnp -6 | grep :80 вернет пустой результат. Добавьте директиву Listen [::]:80:

Listen 0.0.0.0:80
Listen [::]:80
<IfModule ssl_module>
	Listen 0.0.0.0:443
	Listen [::]:443
</IfModule>
<IfModule mod_gnutls.c>
	Listen 443
</IfModule>

Обратите внимание, что директива Listen [::]:443 добавлена по тому же принципу: в блоке <IfModule ssl_module> адрес указан явно, поэтому для IPv6 нужна отдельная строка. Если в вашем файле директива записана без адреса (Listen 443), она уже принимает подключения по обоим протоколам, и добавлять ничего не нужно.

Проверьте конфигурацию и перезапустите Apache:

sudo apache2ctl configtest
sudo systemctl restart apache2

Убедитесь, что созданы сокеты для обоих протоколов:

sudo ss -tlnp | grep :80
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("apache2",pid=45179,fd=4))
LISTEN 0      511             [::]:80           [::]:*    users:(("apache2",pid=45179,fd=6)

В отличие от конфигурации с директивой Listen 80, здесь создаются два отдельных сокета – по одному на каждый протокол.

Обратите внимание!
Если при перезапуске появляется ошибка Address already in use, значит в файле указана директива Listen 80 без адреса, и сокет уже принимает оба протокола. Удалите добавленную строку Listen [::]:80.

Виртуальный хост

Виртуальный хост изменять не требуется – конструкция <VirtualHost *:80> подходит для обоих протоколов, а параметры DocumentRoot, Directory и обработка API от протокола не зависят:

<VirtualHost *:80>
    ServerName example.ru
    DocumentRoot /var/www/example
</VirtualHost>

Изменения нужны только в том случае, если в виртуальном хосте указан конкретный адрес IPv4, например: <VirtualHost 81.200.119.100:80> – в этом случае замените адрес на * либо добавьте отдельный виртуальный хост с адресом IPv6.

Настройка связки Nginx и Apache

В этой схеме Nginx принимает внешние подключения и отдает статические файлы, а Apache слушает только адрес 127.0.0.1 и обрабатывает динамические запросы. Файл /etc/apache2/sites-available/example.conf имеет примерно следующий вид: 

<VirtualHost 127.0.0.1:8080>
    ServerName example.ru
    DocumentRoot /var/www/example/api
</VirtualHost>

Поскольку Apache не принимает подключения извне, настраивать IPv6 требуется только в Nginx. Конфигурацию Apache изменять не обязательно.

Откройте конфигурационный файл сайта:

sudo nano /etc/nginx/sites-available/example.conf

Добавьте строку listen [::]:80; в блок server:

server {
    listen 80;
    listen [::]:80;
    server_name example.ru;
    root /var/www/example/public;
    index index.html;
    location /api/ {
        proxy_pass http://127.0.0.1:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

Если на сайте установлен SSL-сертификат, добавьте строку listen [::]:443 ssl; в блок, отвечающий за 443 порт. Защищенные соединения принимает Nginx, конфигурацию Apache это не затрагивает. 

Проверьте конфигурацию и перезапустите Nginx:

sudo nginx -t
sudo systemctl restart nginx

Проверьте, что порты распределены корректно:

sudo ss -tlnp | grep -E ':80|:8080'
LISTEN 0      511        127.0.0.1:8080      0.0.0.0:*    users:(("apache2",pid=45340,fd=3),("apache2",pid=45339,fd=3),("apache2",pid=45338,fd=3),("apache2",pid=45337,fd=3),("apache2",pid=45336,fd=3),("apache2",pid=45334,fd=3))
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=45392,fd=5),("nginx",pid=45391,fd=5))
LISTEN 0      511             [::]:80           [::]:*    users:(("nginx",pid=45392,fd=6),("nginx",pid=45391,fd=6))

Nginx слушает оба протокола, Apache остается только на 127.0.0.1.

Проверьте работу сайта и API по обоим протоколам, указав путь к своему API вместо /api/:

curl -4 http://127.0.0.1/
curl -6 http://[::1]/
curl -4 http://127.0.0.1/api/
curl -6 http://[::1]/api/

Ответы, полученные по IPv4 и IPv6, должны совпадать. 

Пример того же тестового сайта, обслуживаемого связкой Nginx и Apache:

пример Nginx и Apache

Настройка соединения Nginx и Apache по IPv6

Этот шаг необязательный. Связка работает и в текущем виде: посетители подключаются к Nginx по обоим протоколам, а до Apache запрос доходит по локальному адресу IPv4.

Если внутренний обмен тоже нужно перевести на IPv6, меняются три файла:  

  • адрес прослушивания в файле /etc/apache2/ports.conf:
Listen [::1]:8080
  • адрес в виртуальном хосте Apache, файл /etc/apache2/sites-available/example.conf:
<VirtualHost [::1]:8080>
  • адрес в директиве Nginx, файл /etc/nginx/sites-available/example.conf:
proxy_pass http://[::1]:8080/;

Перезапустите обе службы и проверьте результат – он не должен измениться:

sudo apache2ctl configtest && sudo systemctl restart apache2
sudo nginx -t && sudo systemctl restart nginx
sudo ss -tlnp | grep -E ':80|:8080'
curl -6 http://[::1]/api/
Обратите внимание!
Убедитесь, что прежняя директива Listen 127.0.0.1:8080 удалена, а не оставлена рядом с новой. Иначе Apache не запустится из-за ошибки Address already in use.

Передача IP посетителя в приложение

В связке Nginx и Apache запрос доходит до приложения через Nginx. Поэтому в переменных $_SERVER['REMOTE_ADDR'] и $_SERVER['SERVER_ADDR'] оказывается локальный адрес – 127.0.0.1 или ::1, если внутренний обмен переведен на IPv6. Они описывают соединение между Nginx и Apache, а не подключение посетителя, поэтому ни его адрес, ни протокол по ним определить нельзя.

Адрес посетителя можно передать заголовком X-Forwarded-For. Дополните блок location в файле /etc/nginx/sites-available/example.conf:

location /api/ {
    proxy_pass http://127.0.0.1:8080/;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
}

Проверьте конфигурацию и перезапустите Nginx:

sudo nginx -t
sudo systemctl restart nginx

$remote_addr содержит IP непосредственного клиента Nginx. Если Nginx принимает соединение напрямую от посетителя, в X-Forwarded-For попадет его IPv4-адрес или IPv6-адрес.

Многие приложения и фреймворки умеют работать с X-Forwarded-For. Если приложению нужно различать IPv4 и IPv6, оно определяет версию по адресу из этого заголовка.

Заголовок X-Forwarded-For можно использовать и в логах Apache. Например:

LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b" proxy
CustomLog /var/log/apache2/access.log proxy

В таком формате Apache записывает IP посетителя из X-Forwarded-For, а не адрес Nginx, через который пришел запрос.

X-Forwarded-Proto передает схему исходного запроса – http или https. Он нужен, например, когда HTTPS завершается на Nginx, а до Apache запрос передается по HTTP.

Настройка DNS: AAAA-запись

Пока AAAA-запись не добавлена, сайт остается доступным только по IPv4 – это позволяет проверить настройки, не затрагивая текущих посетителей.

Проверьте доступность сайта напрямую по адресам сервера, подставив свои значения:

curl -4 http://<IPv4-адрес сервера>/
curl -6 http://[<IPv6-адрес сервера>]/

Если на сайте есть API, проверьте и его, указав свой путь:

curl -4 http://<IPv4-адрес сервера>/api/
curl -6 http://[<IPv6-адрес сервера>]/api/

Команды должны вернуть одинаковый результат для обоих протоколов.

После успешной проверки добавьте AAAA-запись в панели управления DNS-зоной сайта, в качестве значения укажите адрес IPv6 сервера – тот, который отображается в выводе команды ip -6 addr show с пометкой scope global.

Запись A удалять не нужно – наличие обеих записей и есть dual stack.

Проверьте, что записи отдаются:

dig A example.ru +short
dig AAAA example.ru +short

Как клиент выбирает протокол

После публикации AAAA-записи выбор протокола происходит на стороне клиента. Получив от DNS оба адреса, браузер или приложение пробует установить соединение по обоим – с небольшим приоритетом в пользу IPv6 – и использует то, которое установилось быстрее. Если соединение по IPv6 не устанавливается, клиент переключается на IPv4. Этот механизм называется Happy Eyeballs и не требует настройки ни на сайте, ни в API.

Проверьте, какой протокол выбирается, командой без указания версии протокола:

curl -v http://example.ru/ 2>&1 | grep "Connected to"
* Connected to example.ru (2001:db8:1a2b:3c4d::1) port 80

Механизм Happy Eyeballs поддерживают браузеры и большинство современных библиотек, но не все клиенты. Программы, использующие простое последовательное подключение, при неисправном IPv6 будут ожидать истечения тайм-аута. Поэтому перед добавлением AAAA-записи важно убедиться, что сайт по IPv6 действительно отвечает.

Обратите внимание!
Если при проверке в браузере сайт открывается по IPv4, это может быть связано с отсутствием поддержки IPv6 у вашего интернет-провайдера. Проверить поддержку можно, например, на сайте test-ipv6.com.

Частые вопросы

По IPv6 сайт открывается по HTTP, но не по HTTPS

В блок server, отвечающий за 443 порт, не добавлена строка listen [::]:443 ssl;. По IPv4 при этом работают оба протокола.

Apache не принимает подключения по IPv6, хотя в выводе apache2ctl -V есть строка IPv4-mapped addresses enabled

В файле ports.conf адрес указан явно: Listen 0.0.0.0:80. В этом случае сокет создается только для IPv4, требуется добавить директиву Listen [::]:80.

Apache не запускается после изменения ports.conf

Ошибка Address already in use означает обратную ситуацию: директива Listen 80 без адреса уже создала сокет для обоих протоколов, и добавленная строка Listen [::]:80 дублирует его. Оставьте только Listen 80.

Команда ss -tlnp -4 не показывает Apache на 80-м порту

Если в ports.conf указана директива Listen 80 без адреса, создается один сокет IPv6, принимающий подключения и по IPv4. Отсутствие отдельного сокета IPv4 в этом случае не является ошибкой.

Изменения в конфигурации не применились

При изменении директив listen используйте restart вместо reload – перезагрузка конфигурации не всегда пересоздает сетевые сокеты.

AAAA-запись добавлена, но сайт по IPv6 недоступен

Проверьте, что AAAA-запись для домена уже отдается. Также убедитесь, что указанный в записи адрес назначен на сетевом интерфейсе сервера – проверьте вывод команды ip -6 addr show.

В логах приложения отображается адрес 127.0.0.1

При использовании обратного прокси не настроена передача заголовка X-Forwarded-For в конфигурации Nginx.

После подключения IPv6 служба стала доступна из интернета, хотя была закрыта файрволом

Фильтрация для IPv4 и IPv6 настраивается отдельно, и правила для IPv4 на подключения по IPv6 не действуют. Настройте для IPv6 такие же ограничения, как для IPv4.

Часть посетителей жалуется на медленное открытие сайта после добавления AAAA-записи

Проверьте доступность сервера по IPv6 с внешнего устройства. Если IPv6 отвечает не всегда, клиенты без поддержки Happy Eyeballs ожидают истечения тайм-аута перед переключением на IPv4. До устранения причины AAAA-запись можно временно удалить.

Сайт был недоступен несколько секунд во время настройки

Команда restart останавливает и запускает веб-сервер заново, и в этот момент соединения не обслуживаются. Выполняйте перезапуск только после успешной проверки конфигурации.

Если возникнут вопросы, напишите нам, пожалуйста, тикет из панели управления аккаунта (раздел “Помощь и поддержка”), а если вы захотите обсудить возможность подключения IPv4 или IPv6 с коллегами по цеху и сотрудниками Beget – ждем вас в нашем сообществе в Telegram.