VPS в России с ipv6/56/48 в routed

Здравствуйте. Задумался о нормальной настройке ipv6 в России, но столкнулся с полным отсутствием хоть какой-то поддержки даже у хостеров. Расскажите пожалуйста о своём опыте. Знаете ли вы хоть одного хостера кто делегирует ipv6 префикс в России в routed, не on-link. aeza делает это в on-link, что является безумием ещё более высокого уровня, чем выделение куцых копеешных /64. Или расскажите другие способы, как вы решаете эти проблемы в РФ.

Я и иностранных таких не встречал.

Вариант раз, костыльный: явно прописать все нужные адреса на интерфейс (если их не слишком много и нет намерения отдавать их дальше куда-нибудь в VPN)

Вариант два, правильный: иcпользовать NDP proxy (ndppd)

Неправильно прочитал вопрос.

Дублирует вопрос:
Выбор VPS сервиса для России - #61 by anon27947102

На аезе /48 маршрутизируемый из коробки. На вдсине /64 с фильтрацией NAM, необходимо в поддержку написать тикет для отключения фильтрации.

Спасибо за ответ. Мне сегодня лично админ aeza в телеграмме написал, что, конечно, /48 не маршрутизируемый, а on-link. /48 как целый префикс это безумно жирно. Но, как выяснилось, они не маршрутизируют ничего. /64 просто не нужен.
Если можете, то тогда объясните мне несколько ньюансов. /48 on-link значит, что они дают еёо. Они раздают её всем, как общую на какую-то большую часть vpsов за их vlan. Но это разве не вызывает широковещательный шторм из-за ndp и прочие радотости безумия? Разве по стандарту вообще не запрещается делать on-link короче /64?

Я бы не сказал, что он правильный, он скорее тоже костыльный. Ни чем объяснить такое поведение хостеров нельзя.

В tcpdump не вижу шторма.

Тоже по стандарту RFC работает. А как ещё правильно, анонсировать целые префиксы, гоняя ненужный трафик?

UPD:
Я, кажется, понял, вы про режим static? Да, он не рекомендуется для больших подсетей, и я его не использую. В режиме auto всё нормально.

UPD2:

Какую задачу нужно решить, раздать клиентам через тунель IPv6 GUA? Я себе сделал два варианта: с перенаправлением широковещательных NDP через ndppd и анонсом через скрипты OpenVPN где широковещательные пакеты не ходят.

Я не понимаю вопроса. Какой ненужный трафик?

Они просто должны анонсировать префикс, а не лепить общую площадку для всех. Решения хостеров выглядят очень странным. Насколько я понимаю, по RFC 4291 on-link префиксы для сетей с конечными устройствами (а по факту с кучей VPS) должны быть /64, чтобы SLAAC не ломался. Делать префикс короче для такой сети — это прямое нарушение лучших практик. Любой префикс меньше /64 должен означать, что они делегируют его мне полностью, а не заставляют использовать прокси костыли от таких провайдеров.

Какую задачу нужно решить, раздать клиентам через тунель IPv6 GUA?

Да, я хотел бы раздать многим конечным устройствам GUA. Но это не клиенты, это мои конечные устройства, поэтому было бы более чем достаточно нормального /56.

В режиме auto всё нормально.

Вы не могли бы уточнить, что это за режим.

По поводу длины, согласен, RIPE рекомендует выдавать /64 на одного абонента ISP.

В этом режиме ndppd будет перенаправлять NAM пакеты между тунелем и сетью сервера на вышестоящий роутер. Приходящие пакеты на неанонсированные адреса просто отсекутся этим роутером и до сервера не дойдут. Разумеется, тунель должен поддерживать мультикаст передачу на адреса fe80::/10 и ff00::/8 (TAP, например).

Ваш ход с ВПНом мне нравится, это всё переносит на L3. Не могли бы вы раскрыть его более подробно. В этом случае я должен буду на уровне ОС захватывать статически /64, чтобы отправить её в ВПН туннель на роутер?

Вы пишите, что используете два варианта. В чём смысл работать сразу двумя?

Не одновременно, а на разных впс по разному, т.к. использую разные протоколы (несколько резервных тунелей в разные локации).

Необходимо

  1. настроить тунель так, чтобы оправленные с клиента мультикаст пакеты fe80::/10 и ff00::/8 приходили на виртуальный интерфейс сервера (проверяется в tcpdump)
  2. выданный префикс разделить на части: одну для хоста, вторую для раздачи клиентов, прописав хостовую часть в `/etc/network/interfaces` а клиентскую в конфигурацию тунеля
    Пример

    Выдан префикс 2001:db8:dead::/48
    Поделим его на два: 2001:db8:dead:0::/64 и 2001:db8:dead:ffff::/64
    В /etc/network/interfaces надо изменить

    iface eth0 inet6 static
    	address 2001:db8:dead:0::1
    	netmask 64
    

    А 2001:db8:dead:ffff::1/64 использовать для tun

  3. настроить ndppd, например
    ndppd.conf
    route-ttl 30000
      
    proxy eth0 {
       router yes
       timeout 500
       ttl 30000
       rule 2001:db8:dead:ffff::/64 {
          auto
       }
    }
    
  4. включить форвардинг

    sysctl -w net.ipv6.conf.all.forwarding=1
    

    или что-то в этом роде

  5. Настроить политики брандмауэра
  6. firewalld

    Создание внешней зоны

    firewall-cmd --permanent --new-zone="my_external"
    firewall-cmd --permanent --zone="my_external" --set-target=DROP
    firewall-cmd --permanent --zone="my_external" --add-interface=eth0
    firewall-cmd --permanent --zone="my_external" --add-masquerade
    

    Создание зоны тунеля

    firewall-cmd --permanent --new-zone="my_tun"
    firewall-cmd --permanent --zone="my_tun" --add-interface=tun0
    

    Настройка транзитного трафика между зонами

    firewall-cmd --permanent --new-policy="tun_to_exernal"
    firewall-cmd --permanent --policy="tun_to_exernal" --set-target=ACCEPT
    firewall-cmd --permanent --policy="tun_to_exernal" --add-ingress-zone="my_tun"
    firewall-cmd --permanent --policy="tun_to_exernal" --add-egress-zone="my_external"
    

    В политику входящего трафика можно добавлять rich правила

    firewall-cmd --permanent --new-policy="exernal_to_tun"
    firewall-cmd --permanent --policy="external_to_tun" --add-ingress-zone="my_external"
    firewall-cmd --permanent --policy="external_to_tun" --add-egress-zone="my_tun"
    firewall-cmd --permanent --policy="external_to_tun" --add-rich-rule='rule family="ipv6" icmp-type name="echo-request" accept'
    firewall-cmd --permanent --policy="external_to_tun" --add-rich-rule='rule family="ipv6" destination address="2001:db8:dead:ffff::1000" port port="12345" protocol="tcp" accept'
    firewall-cmd --permanent --policy="external_to_tun" --add-rich-rule='rule family="ipv6" destination address="2001:db8:dead:ffff::1000" port port="12345" protocol="udp" accept'
    

    Отклонение остального входящего трафика

    firewall-cmd --permanent --new-policy="external_to_tun_r"
    firewall-cmd --permanent --policy="external_to_tun_r" --set-priority=1
    firewall-cmd --permanent --policy="external_to_tun_r" --add-ingress-zone="my_external"
    firewall-cmd --permanent --policy="external_to_tun_r" --add-egress-zone="my_tun"
    firewall-cmd --permanent --policy="external_to_tun_r" --add-rich-rule='rule family="ipv6" destination address="2001:db8:dead:ffff::/64" reject type="icmp6-adm-prohibited" limit value="1/s"'
    
  7. ?????
  8. PROFIT
Для отладки NDP без тунеля и ndppd можно использовать scapy:
Отладка NDP

В одном окне терминала запускаете tcpdump с фильтром вашей сети

tcpdump -n -i eth0 -U -w ~/eth0.pcap --print "ip6 and (not icmp6 or icmp6[0] != 135 or (icmp6[12] == 0xde and icmp6[13] == 0xad))"

Во втором окне терминала включаете NDP proxy

sysctl -w net.ipv6.conf.all.proxy_ndp=1
sysctl -w net.ipv6.conf.eth0.proxy_ndp=1

Добавляете NDP прокси для эмулируемого адреса 2001:db8:dead:ffff::1000 командой

ip -6 neigh add proxy 2001:db8:dead:ffff::1000 dev eth0

Запускаете инструмент манипуляции пакетов командой:
scapy -H
Отправляете пинг пакет от эмулируемого клиента на quad9:

send(IPv6(src="2001:db8:dead:ffff::1000", dst="2620:fe::9") / ICMPv6EchoRequest())

В результате в первом окне маршрутизатор пришлёт Neighbor Solicitation, прокси ответит NAM, и придёт ответ пинга

Команда принудительного анонса Neighbor Advertisement для машрутизатора:

send(IPv6(src="2001:db8:dead:ffff::1000", dst="ff02::1") / ICMPv6ND_NA(tgt="2001:db8:dead:ffff::1000", R=1, S=1, O=1) / ICMPv6NDOptDstLLAddr(lladdr="мак_адрес_роутера"))

(быть может в адресе источника должен быть link-local адрес хоста, я не помню)

Это очень подробная инструкция. Спасибо. Насколько я вижу, в ней используется именно первый, упомянутый вами метод ndppd, вместе с L2 tap OpenVPN.

Но есть ещё и другой метод с OpenVPN L3, раз там, как вы пишите:

широковещательные пакеты не ходят.

Внесите окончательную ясность. Ваша инструкция уже была очень подробной, вам наверняка не хочется расписывать ещё одну большую инструкцию, но я лишь прошу очень кратко поправить меня в понимании различия этих двух методов.

Ой, я опять перепутал, я забыл, что за L2 и L3 такие. Сейчас напишу.

UPD: в случае когда мультикаст пакеты не бегают, я не использую ndppd с нерекомендуемым static, а использую обработку событий подключения/отключения клиента OpenVPN в дебиан (пункты 2 и 5 предыдущей инструкции актуальны):

Пример server.conf (выданный /64 префикс разделён на два /80)
management ::1 4000

local eth0

port 12345

proto udp4

dev ovpn
dev-type tun

tun-mtu 1448

script-security 2
setenv is_single_instance "yes"
setenv firewalld_zone_names "my_external my_admin"
setenv external_interface_names "eth0"
up event-handler.sh
down event-handler.sh
client-connect event-handler.sh
client-disconnect event-handler.sh

ca ca.crt
cert server.crt
key server.key
crl-verify crl.pem

dh none

topology subnet

server 10.8.0.0 255.255.255.0
server-ipv6 2001:db8:dead:beef:ffff::/80

ifconfig-pool-persist ipp.txt

client-config-dir ccd

push "redirect-gateway def1 bypass-dhcp"
# Пример выдачи IPv6 only
;push "redirect-gateway def1 !ipv4 ipv6"

push "route-ipv6 2000::/3"

push "dhcp-option DNS 10.8.0.1"
push "dhcp-option DNS 2001:db8:dead:beef:ffff::1"

keepalive 10 120

tls-crypt ta.key 0

cipher AES-256-GCM

auth SHA256

max-clients 128

user nobody
group nogroup

persist-key
persist-tun

status openvpn-status.log

verb 3

explicit-exit-notify 1

Где

event-handler.sh с правами 755
#!/bin/bash

function handleFirewallPortPriveleged {
	sudo ${PWD}/event-handler-priveleged.sh "handleFirewallPort" ${1} "${2}" ${3} ${4}
}

function handleNdpProxyPriveleged {
	sudo ${PWD}/event-handler-priveleged.sh "handleNdpProxy" ${1} "${2}"
}

function handleIp6ForwardingPriveleged {
	sudo ${PWD}/event-handler-priveleged.sh "handleIp6Forwarding" ${1} "${2}"
}

function handleNdpNeighborPriveleged {
	sudo ${PWD}/event-handler-priveleged.sh "handleNdpNeighbor" ${1} "${2}" "${3}"
}

function handleFirewallPort {
	if [ -n "${1}" -a ${3} -ge 1 -a ${3} -le 65535 ]; then
		protocol=""

		if [ ${4} == "udp" -o ${4} == "udp4" -o ${4} == "udp6" ]; then
			protocol="udp"
		elif [ ${4} == "tcp-server" -o ${4} == "tcp-server" -o ${4} == "tcp-server" ]; then
			protocol="tcp"
		fi

		if [ -n "${protocol}" ]; then
			handleFirewallPortPriveleged ${1} "${2}" ${3} ${protocol}
		fi
	fi
}

function handleNdpProxy {
	handleNdpProxyPriveleged ${1} "${2}"
}

function handleIp6Forwarding {
	handleIp6ForwardingPriveleged ${1} "${2}"
}

function handleNdpNeighbor {
	handleNdpNeighborPriveleged ${1} "${2}" "${3}"
}

function onUp {
	if [ -n "${firewalld_zone_names}" ]; then
		handleFirewallPort "add" "${firewalld_zone_names}" ${local_port_1} ${proto_1}
	fi
	if [ -n "${external_interface_names}" ]; then
		handleNdpProxy "add" "all ${external_interface_names}"
	fi
	if [ -n "${external_interface_names}" ] || [ -n "${dev}" ] ; then
		handleIp6Forwarding "add" "all ${external_interface_names} ${dev}"
	fi
}

function onDown {
	if [ ${is_single_instance} == "yes" ] && ([ -n "${external_interface_names}" ] || [ -n "${dev}" ]); then
		handleIp6Forwarding "remove" "all ${external_interface_names} ${dev}"
	fi
	if [ ${is_single_instance} == "yes" ] && [ -n "${external_interface_names}" ]; then
		handleNdpProxy "remove" "all ${external_interface_names}"
	fi
	if [ -n "${firewalld_zone_names}" ]; then
		handleFirewallPort "remove" "${firewalld_zone_names}" ${local_port_1} ${proto_1}
	fi
}

function onLearnAddress {
	:
}

function onIpChange {
	:
}

function onConnect {
	if [ -n "${ifconfig_pool_remote_ip6}" ] && [ -n "${external_interface_names}" ]; then
		handleNdpNeighbor "add" "${ifconfig_pool_remote_ip6}" "${external_interface_names}"
	fi
}

function onDisconnect {
	if [ -n "${ifconfig_pool_remote_ip6}" ] && [ -n "${external_interface_names}" ]; then
		handleNdpNeighbor "remove" "${ifconfig_pool_remote_ip6}" "${external_interface_names}"
	fi
}

function onTlsVerify {
	:
}

function onAuthVerify {
	:
}

function onRouteUp {
	:
}

function onRoutePreDown {
	:
}

function onCrResponse {
	:
}

function main {
	if [ -n "${script_type}" ]; then
		if [ ${script_type} == "up" ]; then
			onUp
		elif [ ${script_type} == "down" ]; then
			onDown
		elif [ ${script_type} == "learn-address" ]; then
			onLearnAddress
		elif [ ${script_type} == "ipchange" ]; then
			onIpChange
		elif [ ${script_type} == "client-connect" ]; then
			onConnect
		elif [ ${script_type} == "client-disconnect" ]; then
			onDisconnect
		elif [ ${script_type} == "tls-verify" ]; then
			onTlsVerify
		elif [ ${script_type} == "auth-user-pass-verify" ]; then
			onAuthVerify
		elif [ ${script_type} == "route-up" ]; then
			onRouteUp
		elif [ ${script_type} == "route-pre-down" ]; then
			onRoutePreDown
		elif [ ${script_type} == "client-crresponse" ]; then
			onCrResponse
		fi
	fi
}

main "${@}"

# EOF
event-handler-priveleged.sh с правами 755
#!/bin/bash

function addFirewallPort {
	(set -x; firewall-cmd --zone="${1}" --add-port="${2}/${3}")
}

function removeFirewallPort {
	(set -x; firewall-cmd --zone="${1}" --remove-port="${2}/${3}")
}

function addNdpProxy {
	(set -x; sysctl -w net.ipv6.conf."${1}".proxy_ndp=1)
}

function removeNdpProxy {
	(set -x; sysctl -w net.ipv6.conf."${1}".proxy_ndp=0)
}

function addIp6Forwarding {
	(set -x; sysctl -w net.ipv6.conf."${1}".forwarding=1)
}

function removeIp6Forwarding {
	(set -x; sysctl -w net.ipv6.conf."${1}".forwarding=0)
}

function addNdpNeighbor {
	(set -x; ip -6 neigh add proxy "${1}" dev ${2})
}

function removeNdpNeighbor {
	(set -x; ip -6 neigh del proxy "${1}" dev ${2})
}

function handleFirewallPort {
	action=${1}
	port=${3}
	protocol=${4}
	set -- ${2}
	for zone_name in $@; do
		if [ ${action} == "add" ]; then
			addFirewallPort "${zone_name}" ${port} ${protocol}
		elif [ ${action} == "remove" ]; then
			removeFirewallPort "${zone_name}" ${port} ${protocol}
		fi
	done
}

function handleNdpProxy {
	action=${1}
	set -- ${2}
	for interface_name in $@; do
		if [ ${action} == "add" ]; then
			addNdpProxy "${interface_name}"
		elif [ ${action} == "remove" ]; then
			removeNdpProxy "${interface_name}"
		fi
	done
}

function handleIp6Forwarding {
	action=${1}
	set -- ${2}
	for interface_name in $@; do
		if [ ${action} == "add" ]; then
			addIp6Forwarding "${interface_name}"
		elif [ ${action} == "remove" ]; then
			removeIp6Forwarding "${interface_name}"
		fi
	done
}

function handleNdpNeighbor {
	action=${1}
	address=${2}
	set -- ${3}
	for interface_name in $@; do
		if [ ${action} == "add" ]; then
			addNdpNeighbor "${address}" ${interface_name}
		elif [ ${action} == "remove" ]; then
			removeNdpNeighbor "${address}" ${interface_name}
		fi
	done
}

function main {
	if [ -n "${1}" ]; then
		if [ ${1} == "handleFirewallPort" ]; then
			handleFirewallPort ${2} "${3}" ${4} ${5}
		elif [ ${1} == "handleNdpProxy" ]; then
			handleNdpProxy ${2} "${3}"
		elif [ ${1} == "handleIp6Forwarding" ]; then
			handleIp6Forwarding ${2} "${3}"
		elif [ ${1} == "handleNdpNeighbor" ]; then
			handleNdpNeighbor ${2} "${3}" "${4}"
		fi
	fi
}

main "${@}"

# EOF

Пакет sudo должен быть установлен и надо создать файл

/etc/sudoers.d/openvpn с правами 644
nobody ALL=(ALL) NOPASSWD: /etc/openvpn/event-handler-priveleged.sh

Спасибо за подробный ответ и развернутую инструкцию. Я разобрался с предложенным вами методом для L3 VPN. Если я правильно понял, логика следующая: вместо ndppd вы используете хуки OpenVPN (client-connect/client-disconnect), которые через скрипты и sudo динамически добавляют и удаляют прокси-запись ip -6 neigh add proxy для IP-адреса клиента в туннеле. Это, по сути, ручная и более контролируемая замена ndppd.

Чтобы я мог применить это например к задаче с on-link /48 и правильно пробросить GUA адреса на устройства в домашней сети, у меня осталось несколько уточняющих вопросов по вашей конфигурации:

  1. Вы делите выданный /64 на /80. Насколько я понимаю, server-ipv6 .../80 в конфиге OpenVPN создает лишь пул адресов для самого туннельного соединения. Но как тогда устройства в домашней LAN за роутером получают свои GUA-адреса? Ведь для работы SLAAC им нужна подсеть /64. Или эта /80 подсеть используется только для служебной связи между VPS и роутером, а для LAN предполагается маршрутизация другой, уже полноценной /64 подсети?
  2. Команда ip -6 neigh add proxy добавляет запись для одного конкретного адреса. Означает ли это, что для каждого устройства в домашней сети нужно добавлять свою прокси-запись на VPS? Или вы используете другой механизм, например, ip -6 route add .../64 dev tun0, чтобы ядро VPS знало, куда отправлять всю подсеть целиком, а proxy neigh нужен только для адреса самого роутера?
  3. Есть ли принципиальная причина использовать именно OpenVPN для этого метода? Возможно ли реализовать ту же логику с хуками (PostUp/PostDown) в WireGuard, или есть какие-то ограничения?

Можно обойтись одной подсетью /64:

  1. На внешний интерфейс прописать любой адерс из выданной подсети с префиксом /128.
  2. На интерфейс vpn выбрать другой адрес, но префикс задать /64.
  3. Включить маршрутизацию
    Таким-же методом можно смаршрутизировать /64 от провайдера в локальную сеть.

Спасибо за наводку на этот изящный метод борьбы с хостерми-жлобами. Он выглядит гораздо чище, чем запуск отдельного демона.

Если я правильно понял вашу логику, то этот хак с ядром работает следующим образом:

  1. Мы назначаем на внешний интерфейс (eth0) один-единственный адрес с префиксом /128 из выданной нам /64 подсети.
  2. Этим мы говорим ядру Linux, что только этот конкретный адрес является локальным для eth0, а вся остальная /64 подсеть — нет.
  3. Затем мы назначаем всю /64 подсеть на наш VPN-интерфейс (tun0 или wg0), что эффективно создает для нее маршрут.
  4. Когда на VPS включен форвардинг (net.ipv6.conf.all.forwarding=1), ядро видит, что на него приходят NDP-запросы для сети, которая считается on-link для вышестоящего оборудования, но для которой у него самого есть явный маршрут на другой интерфейс.
  5. В итоге, ядро само, без всяких ndppd, начинает выполнять Proxy NDP: оно отвечает на внешние запросы своим MAC-адресом, а полученные пакеты пересылает в туннель согласно таблице маршрутизации.

Я писал, что моя задача нормально настроить ipv6, я занимаюсь сегментацией своих сетей на несколько vlan, и каждому из них, по стандарту, требуется своя уникальная /64 подсеть, чтобы SLAAC и другие механизмы IPv6 работали корректно. Ваш метод идеально подходит для того, чтобы прокинуть одну единственную подсеть /64 в домашнюю сеть. Если бы у меня была только одна локалка, я бы просто этим не занимался.

Но в вашем решении я увидел универсальный принцип, или вы это и сами имели ввиду. Как мне кажется, можно масштабировать и на мою задачу с on-link /48. Хочу вынести на обсуждение возможное развитие вашей идеи.

Идея масштабирования для on-link /48 и нескольких VLAN:

  • Так же, как в вашем методе, назначаем на eth0 один адрес из /48 пула с префиксом /128.
  • Включаем форвардинг.
  • А теперь, вместо одного маршрута, мы создаем несколько, по одному для каждой /64 подсети, которую хотим использовать дома.

Таким образом, я смогу использовать тот же самый изящный трюк, который вы предожили, но на несколько подсеток? Выглядит как рабочий и чистый план, без лишних демонов. Или я что-то упускаю?

Я, наверное, опять вас не правильно понял. В варианте когда не бегают мультикаст пакеты никак не раздать без передачи служебной информации о подключении. Либо делать через ndppd static.

Да.

Добавление маршрута в любом случае происходит самой службой тунеля.

Других приложений с обработкой событий не видел.

Эти команды выполняются при запуске и остановки службы на сервере. У пиров нет команд.

Самый простой способ раздать GUA устройствам внутри LAN — это настроить по кастомному L3 тунелю, но c поддержкой мультикаст пакетов (могу скинуть в ЛС) и сделать так (но мне это не подошло, т.к. виндовс не поддерживает несколько таблиц маршрутизации — хотел раздавать внутри локальной сети один префикс, а сама система имела в приоритете по метрике интерфейса другой, а в линуксе это можно сделать)

Спасибо, я понял большую часть. Да скиньте, если можете. Я изучаю все предложенные варианты. Буду исследовать, после возможно напишу статью со всеми вариантами, соберу все скрипты, может перепишу на другой язык, если будет возможно (не люблю bash, но только он иногда полностью доступен для всех).

Для раздачи /64 устройствам в LAN тогда лучше раздавать не /64 из выданного префикса /48, а /52 /56 или /60 клиентам тунеля, а с клиента тунеля тоже сделать разделение на /64 для последующей маршрутизации — так будет по рекомендациям RIPE сделано. Мне же хватает по двухоктетным группам разделять — удобнее записывать.

Да, я уже начал прикидывать попытки реализовать /60, даже его будет достаточно для всех моих подсеток.

И всё же меня впечатляет, как aeza просто бросили с барского плеча /48 on-link, гордо заявив, что он весь дается на впс. На деле же это общий стол, а ты дальше разбирайся и жри сам, как умеешь, ведь остальные и этого не дают.

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