Обход DPI провайдера на роутере с OpenWrt, используя только busybox
Всем привет, в свете последних новостей от РосКомНадзора решил я глянуть, как дела с блокировками у моего провайдера. Оказалось, что гугловский DNS не спасает, а блокировка работает путем выделения HTTP запроса на запрещенный сайт и последующего дропания пакетов найденной TCP сессии. Однако после небольшого ковыряния оказалось, что для обхода достаточно одного busybox’а. Кому интересно — велком под кат.
Сразу оговорюсь, что реализация блокировки зависит от провайдера и мой способ может у вас не сработать. Итак, рассматривать будем на примере всем известного сайта rutracker.ogr (домен и IP адрес в посте изменен во избежание).
Начал я с простого запроса, чтобы посмотреть, как в целом реагирует провайдер.
wget -O/dev/null "rutracker.ogr" --2016-02-10 01:21:18-- http://rutracker.ogr/ Распознаётся rutracker.ogr (rutracker.ogr)… 195.82.147.214 Подключение к rutracker.ogr (rutracker.ogr)|195.82.147.214|:80... соединение установлено. HTTP-запрос отправлен. Ожидание ответа...
01:25:10.736021 IP 192.168.5.2.53724 > 195.82.147.214.80: Flags [S], seq 778021515, win 29200, options [mss 1460,sackOK,TS val 40091571 ecr 0,nop,wscale 7], length 0 01:25:10.771529 IP 195.82.147.214.80 > 192.168.5.2.53724: Flags [S.], seq 866160985, ack 778021516, win 14600, options [mss 1400], length 0 01:25:10.771562 IP 192.168.5.2.53724 > 195.82.147.214.80: Flags [.], ack 1, win 29200, length 0 01:25:10.771701 IP 192.168.5.2.53724 > 195.82.147.214.80: Flags [P.], seq 1:141, ack 1, win 29200, length 140: HTTP: GET / HTTP/1.1 01:25:11.129078 IP 192.168.5.2.53724 > 195.82.147.214.80: Flags [P.], seq 1:141, ack 1, win 29200, length 140: HTTP: GET / HTTP/1.1 01:25:11.849176 IP 192.168.5.2.53724 > 195.82.147.214.80: Flags [P.], seq 1:141, ack 1, win 29200, length 140: HTTP: GET / HTTP/1.1 01:25:13.292495 IP 192.168.5.2.53724 > 195.82.147.214.80: Flags [P.], seq 1:141, ack 1, win 29200, length 140: HTTP: GET / HTTP/1.1
Клиент успешно подключается, оправляет запрос и собственно все. Куча TCP перепосылок до истечения таймаута. DPI распознал сессию и дропнул ее как запрещенную. А дальше меня зачем-то дернуло попробовать то же самое, но через telnet.
telnet rutracker.ogr 80 Trying 195.82.147.214... Connected to rutracker.ogr. Escape character is '^]'. GET / HTTP/1.1 User-Agent: Wget/1.16.3 (linux-gnu) Accept: */* Accept-Encoding: identity Host: rutracker.ogr Connection: Keep-Alive HTTP/1.1 301 Moved Permanently Server: nginx Date: Tue, 09 Feb 2016 22:29:50 GMT Content-Type: text/html Content-Length: 178 Location: http://rutracker.ogr/forum/index.php Connection: keep-alive301 Moved Permanently 301 Moved Permanently
nginx
01:33:15.354300 IP 192.168.5.2.53782 > 195.82.147.214.80: Flags [S], seq 4112340002, win 29200, options [mss 1460,sackOK,TS val 40236956 ecr 0,nop,wscale 7], length 0 01:33:15.389921 IP 195.82.147.214.80 > 192.168.5.2.53782: Flags [S.], seq 3472440534, ack 4112340003, win 14600, options [mss 1400], length 0 01:33:15.389967 IP 192.168.5.2.53782 > 195.82.147.214.80: Flags [.], ack 1, win 29200, length 0 01:33:16.226472 IP 192.168.5.2.53782 > 195.82.147.214.80: Flags [P.], seq 1:17, ack 1, win 29200, length 16: HTTP: GET / HTTP/1.1 01:33:16.263181 IP 195.82.147.214.80 > 192.168.5.2.53782: Flags [.], ack 17, win 14600, length 0 01:33:16.263214 IP 192.168.5.2.53782 > 195.82.147.214.80: Flags [P.], seq 17:139, ack 1, win 29200, length 122: HTTP 01:33:16.298317 IP 195.82.147.214.80 > 192.168.5.2.53782: Flags [.], ack 139, win 14600, length 0 01:33:16.789180 IP 192.168.5.2.53782 > 195.82.147.214.80: Flags [P.], seq 139:141, ack 1, win 29200, length 2: HTTP 01:33:16.827756 IP 195.82.147.214.80 > 192.168.5.2.53782: Flags [.], ack 141, win 14600, length 0 01:33:16.828043 IP 195.82.147.214.80 > 192.168.5.2.53782: Flags [P.], seq 1:383, ack 141, win 14600, length 382: HTTP: HTTP/1.1 301 Moved Permanently 01:33:16.828067 IP 192.168.5.2.53782 > 195.82.147.214.80: Flags [.], ack 383, win 30016, length 0 01:33:20.376119 IP 192.168.5.2.53782 > 195.82.147.214.80: Flags [F.], seq 141, ack 383, win 30016, length 0 01:33:20.412142 IP 195.82.147.214.80 > 192.168.5.2.53782: Flags [F.], seq 383, ack 142, win 14600, length 0 01:33:20.412177 IP 192.168.5.2.53782 > 195.82.147.214.80: Flags [.], ack 384, win 30016, length 0 01:33:30.299143 IP 192.168.5.2.53780 > 195.82.147.214.80: Flags [FP.], seq 1515330593:1515330733, ack 3791059975, win 29200, length 140: HTTP: GET / HTTP/1.1
Собственно в этот момент стало понятно, что не так страшен DPI, как его малют. Если присмотреться к дампам, то видно, что telnet отправляет первую строку запроса отдельным пакетом. То есть DPI анализирует не TCP поток, а первый пакет с данными от клиента к серверу и пытается из пути и поля Host собрать Url, чтобы пробить его по базе. Если же первую строку запроса с путем и строку с полем Host растащить по разным пакетам, то DPI не может корректно обработать такую сессию и пропускает ее.
Дело оставалось за малым. Нужно было сделать некий прокси, который бы разбивал заголовок первого HTTP запроса в TCP сессии (помним про Connection: Keep-Alive) на два пакета, а остальное бы просто пересылал насквозь. Также хотелось, чтобы этот прокси работал на роутере под OpenWrt, дабы обеспечить всю домашнюю сеть. Сделать такой прокси можно разными способами, я выбрал самый ленивый, быстрый и наколенный, так как цель была сделать именно proof of concept, а не коробочное решение.
В качестве прокси у меня выступает shell скрипт, который запускается из-под tcpsvd (по умолчанию в OpenWrt’шном busybox’е апплет tcpsvd отсутствует, поэтому его надо пересобрать, используя стандартный buildroot). На всякий случай напомню, что tcpsvd — это такая штука, которая слушает порт и при подключении клиента запускает дочерний процесс, перенаправляя его ввод/вывод в сокет.
Получился вот такой скрипт (ногами просьба не бить, это всего лишь proof of concept)
#!/bin/sh
# костыль для склейки строк через символы перевода строки
appendLine()
{
if [[ ! -z "$1" ]]
then
echo -ne "$1\r\n$2"
else
echo -ne "$2"
fi
}
header1=""
header2=""
host=""
while [[ true ]]
do
# читаем строку
read -r line
# обрезаем оставшийся в строке 0x0D
line=`echo "$line" | tr -d "\r"`
# конец заголовка
if [[ -z "$line" ]]
then
break
fi
# Все, что до Host: попадет в header1, остальное - в header2
if [[ -z "$host" ]]
then
if [[ `echo "$line" | grep -c "Host:"` -eq "1" ]]
then
host=`echo "$line" | sed -re 's/^Host: (.*)\r?$/\1/'`
header2=`appendLine "$header2" "$line"`
else
header1=`appendLine "$header1" "$line"`
fi
else
header2=`appendLine "$header2" "$line"`
fi
done
{
# первая половина заголовка
echo -ne "$header1\r\n"
# ждем секунду, чтобы netcat отправил пакет, если кто подскажет, как сделать это иначе - скажу спасибо
sleep 1
# вторая половина заголовка
echo -ne "$header2\r\n\r\n"
# заголовок ушел, DPI пропустил сессию, остальное - просто прокидываем
cat 2>/dev/null
} | nc "$host" 80
На всякий случай уточню, что секунду мы ждем только в начале TCP сессии, а так как обычно браузеры сессию не отключают, а продолжаются слать в ней запросы, явных тормозов быть не должно.
Запускается этот костыльный прокси так: tcpsvd 0.0.0.0 3128 ./proxy.sh
Осталось только перенаправить трафик с браузера на прокси любым способом. Можно это сделать например, как тут, а можно сгенерировать файл автоопределения прокси из списка запрещенных сайтов. Второй вариант проще, лень опять победила и получилось вот такое:
{
echo -e "function FindProxyForURL(url, host)\n{\n\tif ("
wget "http://api.antizapret.info/group.php?data=domain" -O- | sed -re 's/^(.*)$/localHostOrDomainIs(host,"\1") || /'
echo -e "false)\n{ return \"PROXY 192.168.5.1:3128\"; }\nreturn \"DIRECT\";\n}";
} > ~/antidpi.pac
После подсовывания этого файла в настройки прокси запрещенные сайты начали открываться без вопросов.
Буду рад, если кто-нибудь менее ленивый сделает прокси по-нормальному или, если я изобрел велосипед, укажет на готовое стандартное решение.
На этом все, спасибо за внимание.