IPv6 для веб-разработчиков
Что нужно знать веб-разработчикам об IPv6: URL со скобками, хранение в базах данных, программирование сокетов и распространённые баги, которых следует избегать.
Если вы игнорировали IPv6, потому что «это будет проблемой на потом», потом — это сейчас. Мобильные сети работают с приоритетом IPv6. Некоторые пользователи за NAT64 могут достичь вас только через IPv6. Даже localhost на вашей машине разработки, вероятно, разрешается в ::1 перед 127.0.0.1.
Это руководство покрывает то, что вам действительно нужно знать: обработка IPv6 в URL, хранение адресов в базах данных, программирование сокетов и баги, которые укусят вас, если вы не будете осторожны.
TL;DR - Краткое резюме
Ключевые моменты:
- IPv6-адреса в URL требуют квадратных скобок:
http://[2001:db8::1]:8080/ - Храните адреса как VARBINARY(16) или INET в базах данных, не VARCHAR
- Используйте
::(dual-stack) для прослушивания сокетов, чтобы принимать IPv4 и IPv6 - Регулярные выражения IPv4 не работают для IPv6 — используйте встроенные функции парсинга
- Тестируйте на реальных IPv6-сетях, не только на localhost
Перейти к: IPv6 в URL | Хранение в базах данных | Программирование сокетов
IPv6 в URL#
IPv6-адреса содержат двоеточия, что конфликтует с разделителем порта в URL. Решение — нотация квадратных скобок:
http://[2001:db8::1]:8080/api/users
https://[2606:2800:220:1:248:1893:25c8:1946]/Без скобок парсеры не могут определить, где заканчивается адрес и начинается порт. Это применяется к:
- HTTP/HTTPS URL
- WebSocket-соединениям (
ws://[::1]:3000) - Строкам подключения к базам данных
- Любой URI-схеме
При программной генерации URL оборачивайте IPv6-адреса в скобки. При парсинге отбрасывайте их перед валидацией или хранением.
Хранение в базах данных#
Классическая ошибка — использование VARCHAR(15) для IP-адресов. Это подходит для IPv4 (максимум 15 символов: 255.255.255.255), но не для IPv6.
PostgreSQL имеет нативный тип INET, который обрабатывает как IPv4, так и IPv6:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
ip_address INET NOT NULL
);
-- Автоматическая валидация и нормализация
INSERT INTO users (ip_address) VALUES ('2001:db8::1');
INSERT INTO users (ip_address) VALUES ('192.0.2.1');Тип INET эффективно хранит адреса, валидирует их при вставке и поддерживает сетевые операции вроде сопоставления подсетей с операторами << и >>.
MySQL не имеет нативного типа, поэтому используйте либо:
VARBINARY(16)— хранить двоичное представление (16 байт для IPv6, 4 для IPv4)VARCHAR(45)— хранить строковое представление (макс. длина развёрнутого IPv6)
Двоичное хранение эффективнее, но требует функций конверсии:
-- Хранить
INSERT INTO users (ip_address) VALUES (INET6_ATON('2001:db8::1'));
-- Получить
SELECT INET6_NTOA(ip_address) FROM users;Нормализация важна. IPv6-адреса имеют несколько валидных представлений:
2001:0db8:0000:0000:0000:0000:0000:00012001:db8::1(сжатый)2001:db8:0:0:0:0:0:1(частично сжатый)
Всегда нормализуйте перед сравнением или индексированием. Большинство библиотек имеют функцию канонической формы.
Программирование сокетов#
Socket API рассматривает IPv4 и IPv6 как отдельные семейства адресов. Современные приложения должны поддерживать оба.
Node.js по умолчанию использует dual-stack сокеты:
const http = require('http');
// Привязывается к :: (все IPv6-адреса) и принимает IPv4 через mapping
const server = http.createServer((req, res) => {
res.end(`Your IP: ${req.socket.remoteAddress}`);
});
server.listen(3000, '::');Адрес :: — IPv6-эквивалент 0.0.0.0, принимающий соединения на всех интерфейсах. Большинство систем поддерживают IPv4-mapped IPv6-адреса (::ffff:192.0.2.1), позволяя одному IPv6-сокету обрабатывать оба протокола.
Python требует явной обработки dual-stack:
import socket
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
sock.bind(('::', 8080))
sock.listen(5)Флаг IPV6_V6ONLY контролирует, принимает ли сокет только IPv6 или оба IPv4 и IPv6.
Go делает dual-stack по умолчанию:
listener, err := net.Listen("tcp", ":8080")
// Автоматически слушает как IPv4, так и IPv6Для клиентских соединений всегда используйте getaddrinfo() (или эквивалент вашего языка) вместо ручного разрешения адресов. Он корректно обрабатывает IPv4, IPv6 и dual-stack сценарии:
import socket
# Не делайте так
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('example.com', 80))
# Делайте так - работает для IPv4 и IPv6
addr_info = socket.getaddrinfo('example.com', 80, socket.AF_UNSPEC, socket.SOCK_STREAM)
sock = socket.socket(addr_info[0][0], addr_info[0][1])
sock.connect(addr_info[0][4])Локальное тестирование#
Большинство систем разрешают localhost как в ::1 (IPv6), так и 127.0.0.1 (IPv4). Проверьте ваш /etc/hosts:
127.0.0.1 localhost
::1 localhostЕсли ваше приложение привязывается только к 127.0.0.1, оно не будет принимать IPv6-соединения. Привяжитесь к :: для dual-stack или явно привяжитесь к обоим адресам.
Для тестирования поведения только IPv6 отключите IPv4 на вашем loopback-интерфейсе или явно используйте [::1] в вашем HTTP-клиенте:
curl http://[::1]:3000/Для реалистичного тестирования используйте IPv6-only тестовую сеть. Многие облачные провайдеры предлагают IPv6-only экземпляры, которые вынуждают вас обрабатывать крайние случаи вроде NAT64/DNS64.
CDN и origin-серверы#
Большинство CDN (Cloudflare, Fastly, AWS CloudFront) поддерживают IPv6 по умолчанию. Они будут обслуживать контент через IPv6 клиентам и транслировать в IPv4 для вашего origin, если нужно.
Загвоздка: если вы выставляете API напрямую (обходя CDN), ваш origin-сервер должен поддерживать IPv6. Проверьте, что ваш DNS имеет AAAA-записи, и ваш файрвол разрешает IPv6-трафик.
Распространённые баги#
Regex-валидация. IPv6 regex-паттерны печально сложны и обычно неправильны:
// Не делайте так
const ipv6Regex = /^([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}$/i;
// Отказывает на :: сжатии, IPv4-mapped адресах, zone ID...Вместо этого используйте правильную библиотеку парсинга. Каждый основной язык имеет такую, которая корректно обрабатывает IPv6. Для валидации используйте наш инструмент валидатора IPv6.
Жёстко закодированные IPv4-адреса. Ищите в вашей кодовой базе паттерны вроде:
const API_SERVER = 'http://192.168.1.100:3000'; // Не работает с IPv6Используйте вместо этого hostnames или поддерживайте оба семейства адресов.
Rate limiting по IP. IPv6-адреса часто меняются из-за расширений приватности. Rate limiting по точному адресу отказывает. Ограничивайте по подсети /64 вместо этого:
// Плохо
const key = `rate:${ipAddress}`;
// Лучше для IPv6
const key = `rate:${ipv6ToSubnet64(ipAddress)}`;Библиотеки без поддержки IPv6. Старые HTTP-клиенты, драйверы баз данных и сетевые библиотеки могут не обрабатывать IPv6. Тестируйте с реальными IPv6-адресами, а не просто localhost. Если библиотека отказывает, проверьте обновления или альтернативы.
Крайние случаи парсинга URL. Убедитесь, что ваш роутер/фреймворк обрабатывает IPv6-адреса в скобках:
GET http://[2001:db8::1]:8080/api/users
Host: [2001:db8::1]:8080Некоторые парсеры неправильно включают скобки в заголовок Host или не могут правильно извлечь адрес.
Чеклист#
Ваше приложение готово к IPv6, когда:
- Столбцы базы данных могут хранить 45-символьные строки или использовать нативные IP-типы
- Серверы привязываются к
::или явно слушают как IPv4, так и IPv6 - Клиентский код использует
getaddrinfo()или эквивалент для разрешения имён - IP-валидация не полагается на regex
- Нет жёстко закодированных IPv4-адресов в конфигурации
- Rate limiting и геолокация обрабатывают IPv6-подсети
- Парсинг URL обрабатывает нотацию скобок
IPv6 больше не опционален. Встройте его с самого начала, и вы избежите болезненной миграции позже.
Связанные статьи#
- Основы IPv6 — Поймите форматы адресов и базовые концепции для разработки
- Типы адресов IPv6 — Изучите различные типы адресов, которые вам нужно обрабатывать в коде
Валидируйте адреса
Используйте валидатор IPv6 и инструмент Ping для тестирования вашего кода с реальными IPv6-адресами.