Pull to refresh

Torrent-клиент на linux-шлюзе — миф или реальность?

Reading time 10 min
Views 5.4K
Обзавёлся я как-то практически случайным образом материнской платой формата Mini-ITX на Intel Atom, и сразу же в голове мелькнула мысль: «Нешумный домашний сервер!..» (блок питания подойдёт ноутбучный + впаянный процессор с пассивным охлаждением). Подумано — сделано!

Докупил корпус, память, один 2.5 HDD (запланировав через пару месяцев взять такой же для зеркала), а так же вторую сетевую карту для раздачи интернета в локальную сеть, и вот на моём почти-сервере красуется новенький (на то время) Debian Squeeze.

Начал думать, о том что я хочу на этом сервере, и, поскольку я из разряда людей, старающихся выжать из имеющегося всё и ещё чуть-чуть, было принято решение поднять web, mail и jabber сервера (белая статика имеется), плюс в качестве «и ещё чуть-чуть» заиметь там же torrent-качалку 24/7.

Всё бы ничего, если бы не одно НО — torrent-клиент на шлюзе. Звучит [не]много бредово, ведь torrent-трафик, идущий в torrent-клиент на шлюзе, забьёт собой весь интернет-канал, оставив с носом устройства локальной сети, не так ли? Но не спешите с выводами: подобный бред вполне себе имеет право на жизнь. Более того, было найдено рабочее решение, которое позволило мирно сосуществовать качалке с другими сервисами на одном сервере, а так же не мешать клиентам локальной сети. Но обо всём по порядку…

1. Немного теории


Я не один раз встречал утверждение «заниматься шейпингом входящего трафика не имеет смысла, т.к. он уже пришёл, а значит занял часть/всю ширину вашего канала у вышестоящей раздающей стороны (например, у провайдера)». Это утверждение верное, но частично, т.к. требует уточнений:

В контексте TCP/IP

Для tcp-соединений имеет место "медленный старт". Это значит, что пакеты между хостами не начнут отправляться сразу с максимальной скоростью. Вместо этого она (скорость) будет увеличиваться/уменьшаться постепенно, исходя из результатов подтверждения приёма пакетов и наличия потерь. Иными словами, скорость отправки пакетов будет «плавать», подстраиваясь под пропускную способность канала между хостами.

Приняв во внимание такое поведение, мы можем где-то между хостами искусственно занижать скорость прохождения пакетов tcp-соединений, тем самым навязывая хостам необходимость снижения скорости передачи.

А вот в случае с udp-пакетами всё несколько печальнее, т.к. отправляющая сторона будет их попросту «кидать», не принимая во внимание ни потери, ни время прохождения пакета (UDP — он и в Африке UDP: негарантированная доставка со всеми вытекающими...). А это уже чревато следующим: если udp-трафик выест всю гарантированную Вам, например, провайдером полосу, то пакеты попросту начнут дропаться, а уже какие это будут пакеты — отдаётся на откуп поставщику интернет-услуг, при этом отправляющая сторона всё так же будет отправлять Вам такой трафик.

Печально? Да, но не на столько, чтобы я отказался от своей идеи загнать torrent-качалку на единственный в доме компьютер, работающий по схеме 24/7 и вот почему: torrent-клиент делает запрос на закачку не всего содержимого раздачи сразу (мол, давай кидай мне всё одним потоком). Вместо этого запросы отправляются относительно некоторых кусков, размер которых варьируется в пределах от 16-ти килобайт до 4 мегабайт. Из чего вытекает, что задерживая закачиваемые части раздачи, можно добиться снижения частоты запросов нашей стороной на закачку других частей, и, как следствие, снизить потребляемую ширину канала torrent-клиентом.

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

В контексте прохождения пакетов через linux-маршрутизотор

Полная схема прохождения пакета через сетевой стек GNU/Linux имеет следующий вид:


Для простоты, давайте рассмотрим ситуацию, в которой пакет направляется из необъятных просторов Интернета на устройство, находящееся за linux-маршрутизатором, и этот пакет уже покинул оборудование провайдера, т.е. следующей точкой будет Ваш WAN-интерфейс.

До того, как попасть в лапы могучего Iptables, пакет пройдёт через ingress-дисциплину, и уже на эту дисциплину мы могли бы «навешать» классы, описывающие их гарантированную и максимальную скорости, но у этой qdisc есть два существенных ограничения:
  1. классы, добавленные в эту дисциплину не могут содержать дочерние классы, а значит и заимствование неиспользуемой ширины дочерними классами у родительского не получится;
  2. трафик, идущий в локальную сеть, «раскидывать» по классам, базируясь на ip нет возможности, т.к. до NAT'а дело ещё не дошло.

В общем, для полноценного динамического шейпинга ingress-дисциплина нам никак не подходит.

После ingress пакет отправляется в Iptables где происходит фильтрация/NAT/маркировка/..., а уже после этого пакет отправляется на egres-дисциплину LAN-интерфейса, через который он (пакет) должен покинуть пределы маршрутизатора и отправится в локальную сеть.

Хочется обратить внимание на то, что:
  • на всём пути прохождения пакета в пределах linux-маршрутизатора есть только одна ingress-дисциплина и одна egress-дисциплина: ingress — на интерфейсе, в который пакет залетает, а egress — на интерфейсе, отпускающем пакеты на волю;
  • пакеты, предназначенные непосредственно маршрутизатору, на egress-дисциплину никогда не попадут (без ухищрений с ifb-устройством).

А вот теперь можно переформулировать утверждение про шейпинг входящего трафика: в GNU/Linux шейпить входящий трафик правильно на исходящем интерфейсе устройства, которое будет стоять до потребителей канала, при этом на свои личные нужды это устройство не должно потреблять трафик (если такая необходимость есть, то это придётся учитывать при формировании классов в шейпере).

2. Обходим ограничения


Как мы уже выяснили, шейпить трафик правильнее на egress-дисциплине, т.е. на исходящем интерфейсе по отношению к направлению движения пакета через маршрутизатор. А это значит, что если запустить torrent-клиент на самом маршрутизаторе, то пакеты, предназначенные для клиента, никогда не будут попадать на egress-дисциплину и «задерживать» их у нас не получится!

Осознав, что на маршрутизаторе, раздающем интернет в локальную сеть, запускать torrent-клиент без жесткого ограничения в нём же потребляемой ширины канала — не фэншуй, задумался над тем, как можно этот трафик шейпить так же, как если бы он шёл в локальную сеть, т.е. направлялся бы сначала на «отдающий»интерфейс, а уже потОм попадал бы в сам torrent-клиент, и чтобы всё это происходило в пределах одной железки?

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

Установив openvz-ядро, мы, помимо изоляции котнейнеров, получаем и venet-интерфейс на хостовой системе, работающий на L3-уровне (для L2 можно использоваться veth-устройства), и весь ip-трафик, летящий в контейнеры, перед этим попадает на venet, т.е. мы получаем исходящий интерфейс. То, что доктор прописал!

Развернув на хосте VZ-контейнеры и раскинув в них torrent-клиент и остальные сервисы, у меня получилась такая вот схема:
OpenVZ


WAN — физический интерфейс, смотрящий в мир
VENET — виртуальный L3-интерфейс, через который проходит весь трафик, идущий в контейнеры (подсеть 192.168.254.0/24)
LAN — физический интерфейс, смотрящий в локальную сеть (подсеть 192.168.0.0/24)

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

У меня ширина канала в мир составляет 100 МБит/с, и абсолютно предсказуемо мне захотелось максимально использовать это в своих корыстных и не очень целях следующим образом:
  1. если в какой-то промежуток времени что-то тянется извне torrent-клиентом (контейнер BitTorrent) и только им, то всю имеющуюся полосу нужно отдать ему
  2. если во время скачивания torrent-клиентом чего-либо кто-то из локальной сети или подсети контейнеров начинает качать/смотреть по http, то torrent-трафик нужно зажать до, допустим, 32Кб/сек, а остальную ширину отдать под http
  3. самое главное — всю ширину канала должны иметь возможность использовать любые устройства независимо от того, в какой подсети они находятся: за LAN-интерфейсом или за VENET-интерефейсом

Звучит неплохо, не правда ли? А если ещё учесть, что пример с http-трафиком 2-го пункта — малая доля требований, реализовав которые можно добиться идиллии «торренты тянутся, пинги бегают, youtube'чик смотрится», то желания воплотить это в жизнь появляется ещё больше.

Начал думать, как обойти проблему, связанную с 3-м пунктом требований, ведь заимствовать неиспользуемую ширину у других сетевых нельзя… Ну что же, раз на двух интерфейсах нам динамического шейпинга не добиться — будем шейпить на ifb-устройстве, предварительно завернув весь трафик c egress-дисциплин LAN и VENET на него.

3. За дело!


Перед заглядыванием под капот, хотелось бы в пару словах рассказать о принципе классификации трафика, который я для себя выбрал:
  • при определении приоритета пакетов играют роль: протокол и номер порта;
  • весь неклассифицированный трафик не может «разгоняться» до максимума, т.к. потом (в случае с torrent-трафиком) крайне неохотно отдаёт заимствованную полосу классу с более высоким приоритетом. Т.е. я оставил зазор для «протискивания» более важных пакетов.

Ну, а теперь приступим.

Определяем параметры, очищаем дисциплины и подгружаем ifb-устройство
#!/bin/bash

IPT="/sbin/iptables"
TC="/sbin/tc"
IP="/bin/ip"

DEV_LAN="eth1" # Интерфейс, смотрящий в локальную сеть
DEV_VENET="venet0" # Интерфейс, смотрящий в контейнеры
DEV_IFB_LAN="ifb0" # Тут будем шейпить входящий с интернета трафик

CEIL_IN="95mbit" # Максимальная ширина канал минус 5%
CEIL_IN_BULK="90mbit" # Максимальная ширина неклассифицированного трафика

# Очищаем дисциплины интерфейсов
$TC qdisc del dev $DEV_LAN root >/dev/null 2>&1
$TC qdisc del dev $DEV_LAN ingress >/dev/null 2>&1

$TC qdisc del dev $DEV_VENET root >/dev/null 2>&1
$TC qdisc del dev $DEV_VENET ingress >/dev/null 2>&1

# Создаём ifb-устройство и поднимаем его
rmmod ifb >/dev/null 2>&1
modprobe ifb numifbs=1
$IP link set $DEV_IFB_LAN up


Создаем дисциплины, классы и фильтры
# Добавляем корневую htb-дисциплину на ifb-устройство
$TC qdisc add dev $DEV_IFB_LAN root handle 1: htb default 80

# Добавляем к этой дисциплине корневой класс, указывая гарантированную ширину
$TC class add dev $DEV_IFB_LAN parent 1: classid 1:1 htb rate $CEIL_IN

    # Дочерние классы, которые будут делить между собой родительскую скорость
    $TC class add dev $DEV_IFB_LAN parent 1:1 classid 1:10 htb rate 5mbit  ceil 5mbit          prio 0
    $TC class add dev $DEV_IFB_LAN parent 1:1 classid 1:20 htb rate 10mbit ceil $CEIL_IN       prio 1
    $TC class add dev $DEV_IFB_LAN parent 1:1 classid 1:30 htb rate 10mbit ceil $CEIL_IN       prio 2
    $TC class add dev $DEV_IFB_LAN parent 1:1 classid 1:40 htb rate 10mbit ceil $CEIL_IN       prio 3
    $TC class add dev $DEV_IFB_LAN parent 1:1 classid 1:50 htb rate 10mbit ceil $CEIL_IN       prio 4
    $TC class add dev $DEV_IFB_LAN parent 1:1 classid 1:80 htb rate 50kbit ceil $CEIL_IN_BULK  prio 7

        # Дисциплины дочерних классов
        $TC qdisc add dev $DEV_IFB_LAN parent 1:10 handle 10: sfq perturb 10
        $TC qdisc add dev $DEV_IFB_LAN parent 1:20 handle 20: sfq perturb 10
        $TC qdisc add dev $DEV_IFB_LAN parent 1:30 handle 30: sfq perturb 10
        $TC qdisc add dev $DEV_IFB_LAN parent 1:40 handle 40: sfq perturb 10
        $TC qdisc add dev $DEV_IFB_LAN parent 1:50 handle 50: sfq perturb 10
        $TC qdisc add dev $DEV_IFB_LAN parent 1:80 handle 80: sfq perturb 10

            # Обеспечиваем деление ширины полосы дисциплины между ip, а не соединениями
            $TC filter add dev $DEV_IFB_LAN parent 10: protocol ip handle 110 flow hash keys dst divisor 512
            $TC filter add dev $DEV_IFB_LAN parent 20: protocol ip handle 120 flow hash keys dst divisor 512
            $TC filter add dev $DEV_IFB_LAN parent 30: protocol ip handle 130 flow hash keys dst divisor 512
            $TC filter add dev $DEV_IFB_LAN parent 40: protocol ip handle 140 flow hash keys dst divisor 512
            $TC filter add dev $DEV_IFB_LAN parent 50: protocol ip handle 150 flow hash keys dst divisor 512
            $TC filter add dev $DEV_IFB_LAN parent 80: protocol ip handle 180 flow hash keys dst divisor 512

#Раскидываем трафик по классам

TC_A_F="$TC filter add dev $DEV_IFB_LAN parent 1:" # Сокращение, чтобы строки не переносились

$TC_A_F  prio 10 protocol ip u32 match ip protocol 6 0xff \
    match u8 0x05 0x0f at 0 \
    match u16 0x0000 0xffc0 at 2 \
    match u8 0x10 0xff at 33 \
    flowid 1:10                                                                                 # ack < 64b
$TC_A_F prio 1 protocol ip u32 match ip protocol 1                           0xff   flowid 1:10 # icmp
$TC_A_F prio 1 protocol ip u32 match ip protocol 6  0xff match ip sport 53   0xffff flowid 1:10 # dns
$TC_A_F prio 1 protocol ip u32 match ip protocol 17 0xff match ip sport 53   0xffff flowid 1:10 # dns

$TC_A_F prio 2 protocol ip u32 match ip protocol 17 0xff match ip tos 0x68   0xff   flowid 1:20 # voip
$TC_A_F prio 2 protocol ip u32 match ip protocol 17 0xff match ip tos 0xb8   0xff   flowid 1:20 # voip
$TC_A_F prio 2 protocol ip u32 match ip protocol 6  0xff match ip sport 8000 0xffff flowid 1:20 # icecast

$TC_A_F prio 3 protocol ip u32 match ip protocol 6  0xff match ip sport 22   0xffff flowid 1:30 # ssh
$TC_A_F prio 3 protocol ip u32 match ip protocol 6  0xff match ip sport 3389 0xffff flowid 1:30 # rdp
$TC_A_F prio 3 protocol ip u32 match ip protocol 6  0xff match ip sport 5222 0xffff flowid 1:30 # jabber c2s
$TC_A_F prio 3 protocol ip u32 match ip protocol 6  0xff match ip sport 5223 0xffff flowid 1:30 # jabber c2s
$TC_A_F prio 3 protocol ip u32 match ip protocol 6  0xff match ip sport 5269 0xffff flowid 1:30 # jabber s2s

$TC_A_F prio 4 protocol ip u32 match ip protocol 6  0xff match ip sport 80   0xffff flowid 1:40 # http
$TC_A_F prio 4 protocol ip u32 match ip protocol 6  0xff match ip sport 443  0xffff flowid 1:40 # https
$TC_A_F prio 4 protocol ip u32 match ip protocol 6  0xff match ip sport 143  0xffff flowid 1:40 # imap
$TC_A_F prio 4 protocol ip u32 match ip protocol 6  0xff match ip sport 993  0xffff flowid 1:40 # imaps
$TC_A_F prio 4 protocol ip u32 match ip protocol 6  0xff match ip sport 25   0xffff flowid 1:40 # smtp
$TC_A_F prio 4 protocol ip u32 match ip protocol 6  0xff match ip sport 465  0xffff flowid 1:40 # smtps
$TC_A_F prio 4 protocol ip u32 match ip protocol 6  0xff match ip sport 587  0xffff flowid 1:40 # smtps
$TC_A_F prio 4 protocol ip u32 match ip protocol 6  0xff match ip sport 21   0xffff flowid 1:40 # ftp

$TC_A_F prio 5 protocol ip u32 match ip protocol 6  0xff match ip sport 20   0xffff flowid 1:50 # ftp


И финальный аккорд: перенаправляем трафик с egress-дисциплин venet0 и eth1 интерфейсов на ifb-устройство
   #  DEV_LAN => DEV_IFB_LAN (ifb0)
   $TC qdisc add dev $DEV_LAN root handle 1: prio
    # пускать мимо шейпера
    $TC filter add dev $DEV_LAN parent 1: prio 1 protocol ip u32 match ip src 192.168.8.253/32 action pass # с ip шлюза в лок.сеть
    $TC filter add dev $DEV_LAN parent 1: prio 1 protocol ip u32 match ip src 192.168.254.0/24 action pass # с vz-контейнеров в лок. сеть
    # остальное, идущее на ingress DEV_LAN, завернуть на egress DEV_IFB_LAN (ifb0)
    $TC filter add dev $DEV_LAN parent 1: prio 2 protocol ip u32 match u32 0 0 action mirred egress redirect dev $DEV_IFB_LAN

#  DEV_VENET -> DEV_IFB_LAN (ifb0)
$TC qdisc add dev $DEV_VENET root handle 1: prio
    # пускать мимо шейпера
    $TC filter add dev $DEV_VENET parent 1: prio 1 protocol ip u32 match ip src 192.168.8.0/24 action pass # с локальной сети в vz-контейнеры
    # остальное, идущее на ingress DEV_VENET, завернуть на egress DEV_IFB_LAN (ifb0)
    $TC filter add dev $DEV_VENET parent 1: prio 2 protocol ip u32 match u32 0 0 action mirred egress redirect dev $DEV_IFB_LAN



Вот и всё. Хочется верить, что изложенное мной решение кому-то поможет, а кого-то сподвигнет и на его усовершенствование.
Tags:
Hubs:
-1
Comments 7
Comments Comments 7

Articles