[Из песочницы] Юникастовая маршрутизация мультикаст-трафика
Предисловие
Недавно мною было замечено, что при просмотре мультикастового IPTV через Wi-Fi часть трафика теряется. После детального изучения проблемы было выяснено, что такое поведение объясняется природой мультикаст-трафика, а именно — MAC-адрес получателя пакета. Он не зависит от получателя и формируется из адреса мультикаст-группы. Соответственно, на такие пакеты претендуют все клиенты, подключенные к беспроводной точке доступа. Вследствие этого нам достается лишь часть пакетов и мы видим обрывистую картинку.
Штатными средствами проблема решается либо созданием отдельной точки доступа для клиента, либо созданием статического маршрута для определенных мультикаст-групп, или же выведением клиента в отдельный VLAN. Вся «сила» таких решений проявится, когда в сети будет несколько IPTV-приставок, желающих посмотреть один и тот же канал, плюс необходимость их в интернете добавит сложность к настройке роутера. Свое решение данной проблемы предлагаю ниже.
Программы типа udpxy здесь не подходят, так как они меняют полную структуру пакета. А нам необходимо лишь установить необходимый MAC-адрес, при этом сохраняя сетевую и транспортную части, чтобы клиентское ПО не заметило никаких изменений.
Данное решение, назовем его MUT (Multicast to Unicast Translation), заключается в следующем:
- Узнать IP-адрес клиента, желающего подключиться к группе
- Сообщить об этом ядру ОС
- Узнать по IP-адресу MAC-адрес клиента
- Создать и отправить копию пакета на соответствующий интерфейс
Выполнение шагов 1 и 2 лежит на программе мультикастовой маршрутизации, 3 и 4 — на ядре. И то и другое требует небольших изменений в своей работе. Вся работа будет проходить в ОС GNU/Linux.
Немного теории
Сетевая маршрутизация IP версии 4 в Linux базируется на следующий структурах:
- sk_buff — самая часто используемая структура и представляет из себя весь сетевой пакет. Она передается из функции в функцию по пути меняя свое содержимое.
- rtable + dst_entry — две структуры, хранящие результат кэширования маршрута, полученный из таблицы маршрутизации. В зависимости от адреса получателя, адреса источника и поля TOS пакета определяется дальнейшая политика по отношению к нему. Эти две структуры хранят важную информацию для нас: интерфейс, через который будет проходить отправка, и поле шлюз — будущий L2-сосед, которому можно отправить пакет, не меняя L3-заголовок. Поиск кэша для каждого кадра производится два раза: один раз на входе (входящий трафик) и второй раз на выходе (исходящий). Нас интересует второй.
- neighbour — каждый экземпляр этой структуры представляет собой L2-соседа для определенного IP-адреса получателя. Он содержит MAC-адрес получателя, полученный после ARP-ответа; очередь из sk_buff, которые необходимо отправить после определения MAC-адреса; таймеры и многое другое. Для мультикаст-групп соседи тоже создаются, только MAC-адрес генерируется функцией. Нам же следует избежать этого.
В Linux маршрутизация мультикаст-трафика полностью контролируется из области пользователя, а именно программой-маршрутизатором. Ключевым элементом в мультикаст-маршрутизации является структура mfc_cache. Это связанный список, который хранит всю информацию о каждом маршруте: адрес источника потока, статистику, дальнейший маршрут и т.д. Добавление и удаление mfc_cache-структур осуществляется пользовательской программой.
Схематическое представление mfc_cache-списка:
Изображение взято из книги «Linux Networking Architecture»
Разработка
За основу было взято ядро Linux 3.18. Для хранения IP-адресов клиентов для каждой мультикаст-группы расширяем mfc_cache связанным списком:
struct mut_dst {
struct list_head list;
__be32 ip;
struct rcu_head rcu;
};
Вводим новую функцию ipmr_unicast_xmit. В ней будет генерироваться юникастовый rtable, но передавать при этом будем мультикастовый sk_buff. Таким образом мы выбираем необходимый интерфейс для будущей отправки.
Теперь для того, чтобы в дальнейшем был создан neighbour для нашего получателя, а не для мультикаст-группы, в rtable указываем шлюз. За это отвечает поле rt_gateway:
struct rtable *rt;
rt = ip_route_output_ports(net, &fl4, NULL, m_dst->ip, 0, 0, 0, IPPROTO_IPIP, RT_TOS(iph->tos), 0);
if (IS_ERR(rt))
goto out_free;
rt->rt_gateway = m_dst->ip;
dev = rt->dst.dev;
Вводим sysctl-переменную /proc/sys/net/ipv4/mut. Она даст возможность смены режима работы ядра «на лету».
sysctl net.ipv4.mut=1 — Включает новый режим
sysctl net.ipv4.mut=0 — Возвращает режим стандартной маршрутизации
Как и раньше можно посмотреть список маршрутов, теперь еще и unicast:
root@multicast:~# cat /proc/net/ip_mr_cache
Group Origin Iif Pkts Bytes Wrong Dsts
0520C3EF 2 18842 25323648 0 01000A0A
Подробнее со всеми изменениями можно ознакомиться в репозитории. Ссылка в конце статьи.
Наглядное представление работы (изменения в колонке с MAC-адресом):
Маршрутизатор
За основу взята программа IGMPProxy. Можно было взять любую другую, тот же mrouted. Очень важно, что все IGMP-сообщения отправляются от IP-адреса запрашивающего интерфейса, и нам ни что не мешает его использовать. Подробности изменений описывать смысла нет, их также можно найти в соответствующем репозитории. Главное то, что в управлении ядра появляются две новые команды, которые должна поддерживать программа:
- MRT_MUT_ADD_DST (212) — добавление получателя
- MRT_MUT_DEL_DST (213) — удаление получателя
Вместе с ними передается структура вида:
struct {
struct in_addr group; // Адрес группы
struct in_addr origin; // Адрес источника
struct in_addr destination; // Адрес клиента
}
Предупреждение
Стоит заметить, что такой подход не дает возможности отключать клиентов от групп за отсутствие от них Membership Report-запросов, так как, исходя из протокола IGMP, клиент, получивший от другого клиента такой запрос с той же группой, сам не отправляет аналогичный. Поэтому отключение возможно только после получения явного Leave Group-пакета.
Использование
Для включения новой возможности необходимо скомпилировать ядро с опцией CONFIG_IP_MUT=y
Для полноценной работы измененной IGMPProxy также необходимо включить CONFIG_SYSCTL_SYSCALL=y
Ссылки
Измененное ядро
Измененный IGMPProxy
Использованная литература
Rami Rosen «Linux Kernel Networking. Implementation and Theory»
Christian Benvenuti «Understanding Linux Network Internals»
Klaus Wehrle and Frank Pahlke «Linux Networking Architecture»
Если у кого-нибудь есть иной способ решения проблемы, прошу поделиться в комментариях.