Обход DPI провайдера на роутере с OpenWrt, используя только busybox

image
Всем привет, в свете последних новостей от РосКомНадзора решил я глянуть, как дела с блокировками у моего провайдера. Оказалось, что гугловский 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
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-alive


301 Moved Permanently

301 Moved Permanently


nginx
Дамп пакетов при запросе через telnet
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

После подсовывания этого файла в настройки прокси запрещенные сайты начали открываться без вопросов.
Буду рад, если кто-нибудь менее ленивый сделает прокси по-нормальному или, если я изобрел велосипед, укажет на готовое стандартное решение.
На этом все, спасибо за внимание.

© Habrahabr.ru