Баг в файрволе pf
Предисловие
В конторе мы вынужденно используем FreeBSD 9.3 на BRAS'ах. Вызвано это тем, что на 10-ой фре есть какой-то косяк с mpd (у нас с его помощью PPPoE-соединения от клиентов обслуживаются). И mpd периодически виснет при значительном изменении числа подключений. Т. е. когда много клиентов одновременно подключается или отключается, mpd зависает в состоянии umtxn. Я писал по этому поводу в уже открытый PR, но движухи по нему нет.
О проблеме
Но речь не об этом. В общем, на фре 9.3 в файрволе pf обнаружился занятный баг. Баг относится к работе NAT на pf. Проявляется так:
- Клиент шлёт через NAT запрос на какой-то сервер в интернете;
- Сервер ему отвечает;
- Но за время ответа у клиента этот порт, с которого он отправлял свой запрос, почему-то закрылся;
- Клиент примет пакет от сервера и ответит серверу ICMP пакетом с типом port unreachable
Тут-то и проявляется баг — pf'овский NAT некорректно натит этот ответный ICMP-пакет, и в итоге с внешнего интерфейса улетает ICMP-пакет с серым SRC IP клиента и белым DST IP того сервера, куда этот пакет должен быть доставлен.
Т. е. допустим, у вас адрес NAT-интерфейса — 1.1.1.1, адрес сервера в интернете 2.2.2.2. И адрес клиента 192.168.0.1. В итоге с ВНЕШНЕГО интерфейса NAT'a улетит пакет с адреса 192.168.0.1 на адрес 2.2.2.2. Т. е. трансляции адреса клиента во внешний адрес NAT (1.1.1.1) не произойдёт.
Кровавые подробности
Если быть совсем точным, то трансляция ICMP-пакета таки происходит. Но не везде и не до конца :-)
Немного теории
ICMP-пакеты подразделяются на несколько типов. Причём в каждом типе есть ещё несколько подтипов. В данном случае клиент генерирует пакет типа 3 «destination unreachable». И подтипа тоже 3 — «port unreacable». Подробности можно почитать в man icmp, там есть табличка с типами и подтипами (подтипы называются codes, коды). В пакет типа port unreacahble клиент вкладывает заголовок того пакета, который вызвал генерацию сообщения ICMP «port unrecahable» (назовём для краткости такой пакет «пакетом-инициатором»). Ну, чтобы сервер, получивший от клиента ICMP destination unreachable понимал, на какой конкретно его пакет клиент ответил, что порт недоступен. И вот расследование показало, что как раз этот заголовок, пакета-инициатора транслируется файрволом нормально. Но сам ICMP-пакет — нет.
Суровая практическая реальность
Итак, мы приняли, что локальный IP клиента у нас 192.168.0.1, а внешний IP, в который NAT'ится клиентский трафик — 1.1.1.1. Таким образом, получается примерно такой формат ICMP-пакета от клиента обратно на сервер 2.2.2.2 (до трансляции):
Заголовок ICMP | SRC IP: 192.168.0.1 | DST IP: 2.2.2.2 |
Заголовок пакета-инициатора | SRC IP: 2.2.2.2 port 80 | DST IP: 192.168.0.1 port 12345 |
То есть, клиент со своего адреса 192.168.0.1 оповещает сервер 2.2.2.2, что типа, «бро, ты мне отправил пакет со своего IP 2.2.2.2 и порта 80 на мой адрес 192.168.0.1 и порт 12345. Ну так вот, довожу до твоего сведения, что этот порт у меня закрыт».
После трансляции через NAT пакет должен модифицироваться следующим образом:
Заголовок ICMP | SRC IP: 1.1.1.1 | DST IP: 2.2.2.2 |
Заголовок пакета-инициатора | SRC IP: 2.2.2.2 port 80 | DST IP: 1.1.1.1 port 54321 |
То есть, NAT уже от своего имени проговаривает серверу 2.2.2.2: «бро, ты мне отправил пакет со своего IP 2.2.2.2 и порта 80 на мой адрес 1.1.1.1 и порт 54321. Ну так вот, довожу до твоего сведения, что этот порт у меня закрыт».
Обращаю внимание, что порт на клиентской стороне ДО и ПОСЛЕ трансляции могут отличаться. Т. е. то, что у клиента слушает на порту 12345, на NAT'е будет на порту 54321 (ну, или на любом другой, который будет свободен на момент установки соединения клиентом, и который NAT выберет для трансляции). Ну это так, для сведения, сейчас это роли не играет.
Так вот, из-за бага в pf пакет транслируется следующим образом:
Заголовок ICMP | SRC IP: 192.168.0.1 | DST IP: 2.2.2.2 |
Заголовок пакета-инициатора | SRC IP: 2.2.2.2 port 80 | DST IP: 1.1.1.1 port 54321 |
Т. е. NAT в pf транслирует только заголовок пакета-инициатора, идущий нагрузкой в ICMP-пакете типа «destination unreachable», но не транслирует заголовок самого ICMP-пакета.
Заключение
Написал сообщение в рассылку freebsd-net. Там товарищ Kristof Provost попросил проверить, есть ли такое же поведение в -CURRENT, и если да, то он посмотрит, в чём там проблема. Ну, соберу -CURRENT на досуге, попробую.
А, кстати, ещё один момент. pf не только некорректно натит такие пакеты, но и также не может блокировать этот трафик. Т. е. правило вида
тоже не работает для таких пакетов. Похоже, pf анализирует только содержащийся в нагрузке заголовок пакета-инициатора, а не самого ICMP-пакета.
UPDATE
Собрал -CURRENT, проверил. Это поведение полностью повторяется и в -CURRENT. Нарисовал багрепорт по этому поводу, авось когда-нибудь починят. На текущий момент подобный пытающийся улететь наружу трафик блокируется с помощью IPFW.