Od wielu miesięcy chodziło mi po głowie zrealizowanie pewnego projektu polegającego na wydzieleniu określonego pasma sieciowego pod qbittorrenta, tak by on sobie działał w tle non stop i konsumował możliwie dużo łącza (download/upload), przy czym bez strat dla pozostałych usług sieciowych. Każdy chyba wie, że gdy odpalimy torrenta na full, to pingi nam zaraz skaczą na paręset ms, czy nawet parę sekund. Dodatkowo jeśli coś pobieramy na torrencie i zachodzi potrzeba pobrania czegoś via firefox, to transfer w najlepszym wypadku rozłoży się w proporcjach 50:50 -- pół łącza przydzielone zostanie qbittorrentowi, a drugi pół firefoxowi. To samo tyczy się uploadu, gdy chcemy coś wysłać np. na dropboxa, wtedy nie ma innego wyjścia jak albo przykręcić kurek qbittorrentowi bezpośrednio w kliencie, albo wyłączyć qbittorrenta zupełnie na czas wysyłania danych do dropboxa. Teoretycznie niby UTP ma łagodzić skutki korzystania z sieci p2p ale coś to nie bardzo u mnie działa. Qbittorrent, co prawda, ma też wbudowany scheduler i można z niego korzystać, np ustawiając transfer wyższy w określonych godzinach, tych, w których wiemy, że nic na kompie nie będziemy robić, czyli w nocy. Ja korzystałem z tego typu rozwiązania przez pewien czas ale ciągle musiałem te godziny przestawiać, w końcu to wyłączyłem i przeszedłem na manualny system zmiany prędkości, przez klikanie tego miernika w qbittorencie, co mnie doprowadzało do szaleństwa.

W każdym razie to przeszłość, od paru dni walczyłem z zaimplementowaniem traffic control, czyli kontroli ruchu sieciowego, bezpośrednio w kernelu, coś co umożliwia dynamiczny przydział łącza w zależności od obciążenia sieci. Oczywiście to jaki wynik chcemy osiągnąć zależy w dużej mierze od konfiguracji ale udało mi się stworzyć setup, który przydziela qbittorrentowi tyle łącza ile jest aktualnie wolnego i nie trzeba przy tym sobie głowy zawracać.

Jest kilka różnych sposobów na osiągnięcie zadowalających nas efektów, ale nie w każdym z nich idzie zrealizować pewne rzeczy. Największe problemy stwarza ruch przychodzący i jeśli chcemy go również kształtować, będziemy musieli się trochę napracować. Oczywiście nic z czym byśmy sobie nie poradzili ale w przypadku qbittorrenta jest to trochę uciążliwe, bo ten lata po portach i adresach ip jak szalony i nie da się stworzyć żadnej prostej reguły by złapać ten ruch. W każdym innym przypadku, kształtowanie ruchu przychodzącego nie będzie raczej nastręczać większych problemów ale my się nie zajmiemy "każdym innym przypadkiem", my tu rozpracujemy jak ogarnąć ruch p2p, tak by człowiek nie bał się odpalić qbittorrenta, bo ten mu uniemożliwi przeglądanie pornusów czy fejsa...

Jak zatem kształtować ruch? Do dyspozycji mamy kilka narzędzi -- podstawowe to tc z pakietu iproute2 . To za jego pomocą wyznaczymy kolejki na interfejsach sieciowych, do których będą napływać pakiety. To tu również będą ustawione gwarantowane limity łącza, oraz jak dużo pasma może zapożyczyć sobie dana kolejka. Jednak samo stworzenie kolejek nic nam nie da, pakiety trzeba jakoś do nich przekierować i tu, w zależności od tego co tak naprawdę chcemy osiągnąć, możemy skorzystać z filtra tc albo też zaciągnąć do pomocy iptables albo nawet i cgroups. Należy pamiętać przy tym, że cgroups może oznaczać tylko (chyba) pakiety wychodzące i nie da rady go użyć na ruchu skierowanym do naszej maszyny.

1. Interfejsy IMQ

Na sam początek walimy z grubej rury, czyli spróbujemy kształtować ruch zarówno wychodzący jak i przychodzący. Najprościej to zrobić przy pomocy interfejsów IMQ ale kernel debianowy (jak i każdy inny) domyślnie nie obsługuje ich. Podobnie też jest z iptables, gdyż nie ma jak przekierować ruchu na te interfejsy -- no nieźle. Bez rekompilacji kernela i iptables się niestety nie obejdzie. Musimy założyć patche IMQ, dostępne pod tym linkiem http://www.linuximq.net/patches.html [9]. Musimy pobrać patch dla kernel 3.12.4+ oraz patch dla iptables 1.4.13.x . Musimy także skołować źródła kernela oraz iptables. Źródła iptables można pobrać z repo debiana:

# apt-get -t testing source iptables
Reading package lists... Done
Building dependency tree
Reading state information... Done
Selected version '1.4.21-1' (testing) for iptables
Need to get 609 kB of source archives.
Get:1 http://ftp.pl.debian.org/debian/ testing/main iptables 1.4.21-1 (dsc) [1,290 B]
Get:2 http://ftp.pl.debian.org/debian/ testing/main iptables 1.4.21-1 (tar) [547 kB]
Get:3 http://ftp.pl.debian.org/debian/ testing/main iptables 1.4.21-1 (diff) [60.6 kB]
Fetched 609 kB in 0s (644 kB/s)
dpkg-source: info: extracting iptables in iptables-1.4.21
dpkg-source: info: unpacking iptables_1.4.21.orig.tar.bz2
dpkg-source: info: unpacking iptables_1.4.21-1.debian.tar.gz
dpkg-source: info: applying 0101-changelog.patch
dpkg-source: info: applying 0102-add_manpages.patch
dpkg-source: info: applying 0103-lintian_allows_to.patch
dpkg-source: info: applying 0104-lintian_hyphens.patch
dpkg-source: info: applying 0105-lintian_spelling.patch
dpkg-source: info: applying 0201-660748-iptables_apply_man.patch
dpkg-source: info: applying 0202-725413-sctp_man_description.patch
dpkg-source: info: applying 0301-install_iptables_apply.patch
dpkg-source: info: applying 0401-580941-iptables_apply_update.patch

Przy czym trzeba dodać deb-src do /etc/apt/sources.list

deb     http://ftp.pl.debian.org/debian/ testing main non-free contrib
deb-src http://ftp.pl.debian.org/debian/ testing main non-free contrib

Mając odpowiednią łatę na iptables, nakładamy ją na źródła:

# cd iptables-1.4.21/
# patch -p1 < ../iptables-1.4.13-IMQ-test1.diff
patching file extensions/libxt_IMQ.c
patching file extensions/libxt_IMQ.man
patching file include/linux/netfilter/xt_IMQ.h

I budujemy paczuszkę deb:

# aptitude build-dep iptables
# dpkg-buildpackage -uc -us -b

Ze źródłami kernela jest trochę gorzej, bo te debianowe mają zaaplikowane własne patche, co trochę uniemożliwia bezstresowe założenie patcha IMQ. Niby na stronie IMQ jest tam wzmianka o tym problemie ale nie mam zielonego pojęcia co powinno zostać zmienione, by ten patch wszedł bez problemu. W każdym razie, jeśli nie debianowy kernel, to trzeba pobrać źródła z oficjalnej strony kernela, https://www.kernel.org/ [10] i tym przypadku jest to 3.12.9 .

Po pobraniu źródeł kernela sprawdzamy podpis:

# xz -cd linux-3.12.9.tar.xz | gpg --verify linux-3.12.9.tar.sign -
gpg: Signature made Sat 25 Jan 2014 06:22:11 PM CET using RSA key ID 6092693E
gpg: Good signature from "Greg Kroah-Hartman (Linux kernel stable release signing key) greg@kroah.com"
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: 647F 2865 4894 E3BD 4571  99BE 38DB BDC8 6092 693E

Jeśli by były jakieś problemy z weryfikacją podpisu, na stronie kernela [11] jest dokładna rozpiska co i jak.

Łatamy kernela:

# tar xfp linux-3.12.9.tar.xz
# xz -d linux-imqmq-3.12.4+.patch.xz
# cd linux-3.12.9/
# patch -p1 < ../linux-imqmq-3.12.4+.patch
patching file drivers/net/Kconfig
patching file drivers/net/Makefile
patching file drivers/net/imq.c
patching file include/linux/imq.h
patching file include/linux/netfilter/xt_IMQ.h
patching file include/linux/netfilter_ipv4/ipt_IMQ.h
patching file include/linux/netfilter_ipv6/ip6t_IMQ.h
patching file include/linux/skbuff.h
Hunk #6 succeeded at 2658 (offset 5 lines).
patching file include/net/netfilter/nf_queue.h
patching file include/uapi/linux/netfilter.h
patching file net/core/dev.c
patching file net/core/skbuff.c
patching file net/ipv6/ip6_output.c
patching file net/netfilter/Kconfig
patching file net/netfilter/Makefile
patching file net/netfilter/core.c
patching file net/netfilter/nf_internals.h
patching file net/netfilter/nf_queue.c
patching file net/netfilter/xt_IMQ.c

Kopiujemy starą konfigurację kernela i odpalamy menuconfig:

# cp /boot/config-3.12-1-amd64 ./.config
# make menuconfig

     -> Device Drivers --->
       -> Network device support --->
         -> Network core driver support --->
             <M>     IMQ (intermediate queueing device) support

     -> Networking support --->
       -> Networking options --->
         -> Network packet filtering framework (Netfilter) --->
           -> Core Netfilter Configuration --->
               <M>   "IMQ" target support

Zapisujemy konfigurację, kompilujemy i robimy paczuszkę deb:

# aptitude build-dep linux-image-`uname -r`
# make-kpkg -j2 --initrd kernel-image kernel-headers

Po kompilacji powinniśmy mieć 4 paczki, instalujemy je:

# dpkg -i iptables_1.4.21-1_amd64.deb libxtables10_1.4.21-1_amd64.deb linux-image-3.12.9-morfik-imq_amd64.deb linux-headers-3.12.9-morfik-imq_amd64.deb

Musimy jeszcze załadować odpowiednie moduły na starcie systemu:

# cat /etc/modules
...
imq numdevs=2
ipt_IMQ
...

Jeśli wszystko przebiegło bez problemów, możemy zresetować maszynę.

1.1. Konfiguracja przepływu

Powinniśmy mieć już w systemie interfejsy IMQ, sprawdźmy zatem czy tak faktycznie jest:

# ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN mode DEFAULT group default qlen 1000
    link/ether 00:e0:4c:75:03:09 brd ff:ff:ff:ff:ff:ff
3: imq0: <NOARP> mtu 16000 qdisc htb state DOWN mode DEFAULT group default qlen 11000
    link/void
4: imq1: <NOARP> mtu 16000 qdisc htb state DOWN mode DEFAULT group default qlen 11000
    link/void

Jak widać, nie są jeszcze aktywowane, podnieśmy je:

# ip link set imq0 up
# ip link set imq1 u

W tej chwili możemy już kierować ruch do nich, potrzebujemy jeszcze odpowiednich kolejek i konfiguracji iptables. Kolejki tworzymy przy pomocy narzędzia tc:

# tc qdisc add dev imq0 root handle 1:0 htb default 4
# tc class add dev imq0 parent 1:0 classid 1:1 htb rate 950kbit ceil 950kbit 
# tc class add dev imq0 parent 1:1 classid 1:2 htb rate 250kbit ceil 950kbit prio 5
# tc class add dev imq0 parent 1:1 classid 1:3 htb rate 450kbit ceil 950kbit prio 0
# tc class add dev imq0 parent 1:1 classid 1:4 htb rate 250kbit ceil 950kbit prio 2

# tc qdisc add dev imq1 root handle 2:0 htb default 4
# tc class add dev imq1 parent 2:0 classid 2:1 htb rate 9500kbit ceil 9500kbit
# tc class add dev imq1 parent 2:1 classid 2:2 htb rate 2500kbit ceil 9500kbit prio 5
# tc class add dev imq1 parent 2:1 classid 2:3 htb rate 4500kbit ceil 9500kbit prio 0
# tc class add dev imq1 parent 2:1 classid 2:4 htb rate 2500kbit ceil 9500kbit prio 2

Powyższe linijki stworzą nam 3 klasy dla każdego interfejsu. Do kolejek 1:2 i 2:2 będzie szedł ruch z qbittorrenta, przez 1:3 i 2:3 będzie przepuszczony ruch użytkownika root oraz morfik, a na klasy 1:4 oraz 2:4 będzie szło wszystko co się nie załapie do powyższych. W tym przypadku korzystam z algorytmu HTB ale jest też kilka innych, a wszystkie można podzielić na dwie kategorie -- bezklasowe (pfifo, bfifo, pfifo_fast, red, sfq, tbf) i klasowe (CBQ, HTB, PRIO). Informacje na temat każdego z powyższych algorytmów można znaleźć w manie tc. Jest też całkiem przyzwoity opis każdego z algorytmów, dostępny pod tym linkiem [12].

Składnia HTB jest prosta i tam gdzie nie precyzujemy opcji, są one ustawiane na wartości domyślne. W powyższym przykładzie tworzymy qdisc na interfejsie imq0 i określamy 4 jako domyślą kolejkę, do której będzie trafiał ruch. Jeśli nie określimy domyślnej kolejki, ruch który nie trafi do żadnej z kolejek, nie będzie ulegał kształtowaniu. Zakładamy główną kolejkę (1:1) i ustawiamy jej limit 950kbitów. Mam, co prawda, 1mbit uploadu ale zaleca się by ten limit był troszeczkę mniejszy niż maksymalna przepustowość łącza, bo musimy mieć najwęższe ogniwo w tym całym procesie przesyłania informacji, by móc kształtować ruch. Następnie tworzymy 3 kolejki wewnątrz wyznaczonego pasma, odpowiednio nadając im parametr rate i ceil. Parametr rate odpowiada za gwarantowaną przepustowość jaką otrzyma kolejka. W przypadku gdy łącze będzie obciążone na full, kolejka 1:2 będzie miała do dyspozycji 250kbitów i nikt jej tego nie pozbawi. Podobnie z pozostałymi kolejkami. Parametr ceil z kolei ustawia górny limit, czyli ile kolejka może pożyczyć łącza od innych kolejek ale tylko w przypadku gdy te nie wykorzystują swojego pasma w pełni. Przykładowo, qbittorrent ma przeznaczone 250kbitów uploadu, gdy osiągnie ten limit sprawdzi czy może sobie coś pożyczyć i jeśli łącze nie będzie obciążone, to zwiększy sobie dostępne zasoby do 950kbitów. Jak tylko jakiś proces, który jest przypisany do pozostałych dwóch kolejek będzie chciał skorzystać z internetu, natychmiast zostanie przykręcony kurek qbittorrentowi. Jeśli proces zakończy buszowanie po internecie i zwolni zasoby, qbittorrent automatycznie zacznie wykorzystywać łącze w pełni ponownie i tak dalej. Podobnie z pobieraniem danych z internetu. Ważne jest by ustawić jeszcze odpowiedni priorytet. Jeśli kolejka ma wyższy priorytet (mniejszy numer), będzie miała dostęp do niewykorzystanych zasobów jako pierwsza, tj. jeśli kolejka 1:2 i 1:3 będą zjadać 100% swojego przydziału, zostanie im rywalizacja o pozostałe 250kbitów dostępnych w kolejce 1:4. Jeśli grupa 1:3 będzie miała wyższy priorytet i będzie chciała dodatkowe 250kbitów, będzie mogła skorzystać z dostępnego pasma 1:4 w pełni, a qbittorrent na linii 1:2 będzie musiał ograniczyć zużycie do 100% swojego pasma. Dopiero w przypadku gdy kolejka 1:3 nie będzie w pełni wykorzystywać zasobów kolejki 1:4, qbittorrent (1:2) będzie mógł skorzystać z dodatkowych zasobów kolejki 1:4. I o takie zachowanie nam chodzi.

Mamy zatem zrobionych parę kolejek. Trzeba jeszcze jakoś przekierować tam ruch. Najprościej to zrobić przy pomocy iptables:

# iptables -t mangle -A PREROUTING -i eth0 -j IMQ --todev 1
# iptables -t mangle -A PREROUTING -j CONNMARK  --restore-mark

# iptables -t mangle -N IMQ-OUT
# iptables -t mangle -A POSTROUTING -o eth0 -j IMQ-OUT
# iptables -t mangle -A IMQ-OUT -m owner --gid-owner p2p -j MARK --set-mark 2
# iptables -t mangle -A IMQ-OUT -m owner --gid-owner p2p -j RETURN
# iptables -t mangle -A IMQ-OUT -m owner --uid-owner morfik -j MARK --set-mark 3
# iptables -t mangle -A IMQ-OUT -m owner --uid-owner morfik -j RETURN
# iptables -t mangle -A IMQ-OUT -m owner --uid-owner root -j MARK --set-mark 3
# iptables -t mangle -A IMQ-OUT -m owner --uid-owner root -j RETURN
# iptables -t mangle -A POSTROUTING -j CONNMARK  --save-mark
# iptables -t mangle -A POSTROUTING -o eth0 -j IMQ --todev 0

Ten patch co założyliśmy na iptables daje nam możliwość zastosowania celu -j IMQ, a ten przyjmuje tylko argument --todev i numer interfejsu. Zatem przekierowaniem ruchu na interfejsy zajmują się te dwie linijki z -j IMQ. W zależności czy jest to ruch wychodzący czy przychodzący, pakiety biegną odpowiednio do interfejsów imq0 i imq1. Reguły z -j CONNMARK zapiszą oznaczenia w /proc/net/ip_conntrack , a samo oznaczenie dokonuje się przez -j MARK --set-mark. Trzeba jednak uważać by czasem nie nadpisać marków, bo jeśli pakiet przechodzić przez pierwszą pozycję z -j MARK , to nie kończy jego podróży w łańcuchu, dlatego właśnie został zrobiony osobny łańcuch do markowania pakietów i tam skorzystaliśmy z -j RETURN -- jak tylko pakiet zostanie przypasowany, zostaje zwrócony do łańcucha wyżej, w tym przypadku POSTROUTING i biegnie sobie dalej przez reguły tego łańcucha. Bez -j RETURN, pakiet by przeszedł przez wszystkie reguły w łańcuchu IMQ-OUT, co w pewnych sytuacjach ustawi złego marka. Mamy dodatkowo większe opóźnienie spowodowane dłuższą trasą pakietu. W powyższym przykładzie został wykorzystany moduł -m owner jako baza przy oznaczaniu pakietów i pakiety konkretnych użytkowników/grup zostaną oznaczone. Można oczywiście w dowolny sposób oznaczać pakiety, ale za bardzo nie widzę innej opcji by wyłapać ruch qbittorrenta.

Potrzebujemy jeszcze odpowiednich filtrów tc by przekierować konkretne pakiety do przeznaczonych dla nich kolejek:

# tc filter add dev imq0 protocol ip parent 1:0 prio 1 handle 2 fw classid 1:2
# tc filter add dev imq0 protocol ip parent 1:0 prio 5 handle 3 fw classid 1:3
# tc filter add dev imq1 protocol ip parent 2:0 prio 3 handle 2 fw classid 2:2
# tc filter add dev imq1 protocol ip parent 2:0 prio 7 handle 3 fw classid 2:3

Przypisujemy filter do określonego interfejsu (tc filter add dev imq0 protocol ip) nadajemy mu priorytet (prio 1) i ustawiamy znacznik (handle 2), na podstawie którego pakiet zostanie przekierowany do odpowiedniej kolejki (classid 1:2). Liczba w handle musi odpowiada tej w --set-mark .

Powinno działać. Przy pomocy narzędzia bmon możemy zaobserwować cośmy uczynili. Poniżej prezentuje się rozpiska przy oglądaniu przez morfika czegoś na yt:

Interfaces                     | RX bps       pps     %| TX bps       pps     %
  lo                           |    294B        2      |    294B        2
  eth0                         |   1.13MiB    871      |  76.13KiB    545
    qdisc none (pfifo_fast)    |      0         0      |  74.37KiB    545
  imq0                         |  66.84KiB    545      |  67.30KiB    547
    qdisc 1: (htb)             |      0         0      |  67.30KiB    547
      cls :2 (fw)              |      0         0      |      0         0
      cls none (fw)            |      0         0      |      0         0
      cls :3 (fw)              |      0         0      |      0         0
      class 1:1 (htb)          |      0         0      |  67.30KiB    547   58%
->      class 1:2 (htb)        |      0         0      |  55.18KiB    245   181%
        class 1:3 (htb)        |      0         0      |  11.70KiB    299   21%
        class 1:4 (htb)        |      0         0      |    431B        3    1%
  imq1                         |   1.12MiB    866      |   1.13MiB    878
    qdisc 2: (htb)             |      0         0      |   1.13MiB    878
      cls :2 (fw)              |      0         0      |      0         0
      cls none (fw)            |      0         0      |      0         0
      cls :3 (fw)              |      0         0      |      0         0
      class 2:1 (htb)          |      0         0      |   1.13MiB    878   100%
        class 2:2 (htb)        |      0         0      | 278.65KiB    275   91%
        class 2:3 (htb)        |      0         0      | 882.11KiB    602   161%
        class 2:4 (htb)        |      0         0      |     62B        0    0%

Jak można zaobserwować, stan downloadu/uploadu wynosi 100%/58%. Klasa 2:3 konsumuje 161% przewidzianych zasobów dla tej kolejki -- zjada wolny przydział klasy 2:4 i trochę transferu 2:2, bo qbittorrent nie wykorzystuje swojego pasma w pełni. Podobnie jest z uploadem. Kolejka morfika ma wykorzystane nieco ponad 20% przewidzianych środków, dlatego qbittorrent sobie nieco zapożyczył z innych kolejek. I to tak sobie będzie się automatycznie regulować.

W przypadku gdyby łącze nie było obciążone, staty będą się prezentować następująco:

Interfaces                     | RX bps       pps     %| TX bps       pps     %
->lo                           |    196B        2      |    196B        2
  eth0                         |  14.70KiB    115      | 117.72KiB    129
    qdisc none (pfifo_fast)    |      0         0      | 117.71KiB    129
  imq0                         | 116.28KiB    128      | 116.03KiB    129
    qdisc 1: (htb)             |      0         0      | 116.03KiB    129
      cls :2 (fw)              |      0         0      |      0         0
      cls none (fw)            |      0         0      |      0         0
      cls :3 (fw)              |      0         0      |      0         0
      class 1:1 (htb)          |      0         0      | 116.03KiB    129   100%
        class 1:2 (htb)        |      0         0      | 115.76KiB    126   379%
        class 1:3 (htb)        |      0         0      |    164B        2    0%
        class 1:4 (htb)        |      0         0      |    109B        0    0%
  imq1                         |  12.99KiB    111      |  12.99KiB    111
    qdisc 2: (htb)             |      0         0      |  12.99KiB    111
      cls :2 (fw)              |      0         0      |      0         0
      cls none (fw)            |      0         0      |      0         0
      cls :3 (fw)              |      0         0      |      0         0
      class 2:1 (htb)          |      0         0      |  12.99KiB    111    1%
        class 2:2 (htb)        |      0         0      |   7.58KiB    107    2%
        class 2:3 (htb)        |      0         0      |   5.34KiB      3    1%
        class 2:4 (htb)        |      0         0      |     62B        0    0%

I jak widzimy, qbittorrent zjada całe pasmo uploadu, 279% ponad swój przydział. Sprawdźmy zatem jak wyglądają opóźnienia w takiej sytuacji:

$ ping wp.pl -c 10
PING wp.pl (212.77.100.101) 56(84) bytes of data.
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=1 ttl=247 time=22.0 ms
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=2 ttl=247 time=28.4 ms
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=3 ttl=247 time=24.1 ms
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=4 ttl=247 time=25.2 ms
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=5 ttl=247 time=20.3 ms
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=6 ttl=247 time=22.2 ms
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=7 ttl=247 time=23.9 ms
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=8 ttl=247 time=34.3 ms
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=9 ttl=247 time=21.9 ms
64 bytes from www.wp.pl (212.77.100.101): icmp_seq=10 ttl=247 time=23.6 ms

--- wp.pl ping statistics ---
10 packets transmitted, 10 received, 0% packet loss, time 9004ms
rtt min/avg/max/mdev = 20.371/24.638/34.344/3.859 ms

Całkiem nieźle jak na zapchane łącze przez p2p. Taka mała uwaga jeszcze, może i to fajnie działa ale u mnie przy pobieraniu czegoś przez qbittorrenta, download w nim trochę ssie, a właściwie nie bardzo chce ssać. Trzeba nieco upload przykręcić, tak 5-7% powinno wystarczyć, bo jak nie patrzeć przy pobieraniu plików, pakiety również trzeba wysłać. Trochę to dziwne, że qbittorrent tego nie reguluje automatycznie, a może to ja nie potrafię tego skonfigurować.

Gdzieś na necie znalazłem sposób na wyłapanie tych pakietów i nadanie im wyższego priorytetu. U mnie podziałało, także jeśli nie chce nam się ograniczać uploadu w qbittorrencie, możemy dopisać poniższe regułki w iptables i wstawić je na pierwsze miejsce w łańcuchu IMQ-OUT :

# iptables -t mangle -A IMQ-OUT -o eth0 -m length --length 40:68 -j MARK --set-mark 3
# iptables -t mangle -A IMQ-OUT -o eth0 -m length --length 40:68 -j RETURN

Można oczywiście stworzyć osobną kolejkę dla tych pakietów, tak by przy pobieraniu czegoś, nie przydusiły one pozostałych procesów korzystających z internetu.

Przydałoby się jeszcze zrobić skrypt startowy, co by nam te kolejki konfigurował na starcie systemu. W sumie to można poskładać powyższą konfigurację tc i iptables i wrzucić do jednego skryptu, a ten umieścić na odpowiedniej pozycji w autostarcie systemu. Mój skrypt wygląda tak:

### BEGIN INIT INFO
# Provides:          imq
# Required-Start:    mountkernfs $local_fs
# Required-Stop:     $local_fs
# Should-Start:      
# Should-Stop:       
# Default-Start:     S
# Default-Stop:      
# X-Start-Before:    
# X-Stop-After:      
# Short-Description: tc
# Description:       tc configuration for imq interfaces
### END INIT INFO

SCRIPTNAME="imq"

load_tc()
{
    echo -n "Loading tc configuration... "
    tc qdisc add dev imq0 root handle 1:0 htb default 4
    tc class add dev imq0 parent 1:0 classid 1:1 htb rate 950kbit ceil 950kbit 
    tc class add dev imq0 parent 1:1 classid 1:2 htb rate 250kbit ceil 950kbit prio 5
    tc class add dev imq0 parent 1:1 classid 1:3 htb rate 450kbit ceil 950kbit prio 0
    tc class add dev imq0 parent 1:1 classid 1:4 htb rate 250kbit ceil 950kbit prio 2

    tc qdisc add dev imq1 root handle 2:0 htb default 4
    tc class add dev imq1 parent 2:0 classid 2:1 htb rate 9500kbit ceil 9500kbit
    tc class add dev imq1 parent 2:1 classid 2:2 htb rate 2500kbit ceil 9500kbit prio 5
    tc class add dev imq1 parent 2:1 classid 2:3 htb rate 4500kbit ceil 9500kbit prio 0
    tc class add dev imq1 parent 2:1 classid 2:4 htb rate 2500kbit ceil 9500kbit prio 2

    tc filter add dev imq0 protocol ip parent 1:0 prio 1 handle 2 fw classid 1:2
    tc filter add dev imq0 protocol ip parent 1:0 prio 5 handle 3 fw classid 1:3
    tc filter add dev imq1 protocol ip parent 2:0 prio 3 handle 2 fw classid 2:2
    tc filter add dev imq1 protocol ip parent 2:0 prio 7 handle 3 fw classid 2:3
    echo "done!"
}

load_iptables()
{   
    echo -n "Loading iptables configuration... "
    iptables -t mangle -A PREROUTING -i eth0 -j IMQ --todev 1
    iptables -t mangle -A PREROUTING -j CONNMARK  --restore-mark

    iptables -t mangle -N IMQ-OUT
    iptables -t mangle -A POSTROUTING -o eth0 -j IMQ-OUT
   iptables -t mangle -A IMQ-OUT -o eth0 -m length --length 40:68 -j MARK --set-mark 3
   iptables -t mangle -A IMQ-OUT -o eth0 -m length --length 40:68 -j RETURN
    iptables -t mangle -A IMQ-OUT -o eth0 -m owner --gid-owner p2p -j MARK --set-mark 2
    iptables -t mangle -A IMQ-OUT -o eth0 -m owner --gid-owner p2p -j RETURN
    iptables -t mangle -A IMQ-OUT -o eth0 -m owner --uid-owner morfik -j MARK --set-mark 3
    iptables -t mangle -A IMQ-OUT -o eth0 -m owner --uid-owner morfik -j RETURN
    iptables -t mangle -A IMQ-OUT -o eth0 -m owner --uid-owner root -j MARK --set-mark 3
    iptables -t mangle -A IMQ-OUT -o eth0 -m owner --uid-owner root -j RETURN
    
    iptables -t mangle -A POSTROUTING -j CONNMARK  --save-mark
    iptables -t mangle -A POSTROUTING -o eth0 -j IMQ --todev 0
    echo "done!"
}

interfaces_up()
{
    ip link set imq0 up
    ip link set imq1 up
}

flush_tc()
{
    echo -n "Removing tc qdiscs... "
    tc qdisc del dev eth0 root 2> /dev/null
    tc qdisc del dev eth0 ingress 2> /dev/null
    tc qdisc del dev imq0 root 2> /dev/null
    tc qdisc del dev imq1 root 2> /dev/null
    echo "done!"
}

flush_iptables()
{
    echo -n "Flushing mangle table... "
    iptables -t mangle -F 2> /dev/null
    iptables -t mangle -X IMQ-OUT 2> /dev/null
    echo "done!"
}

interfaces_down()
{
    ip link set imq0 down
    ip link set imq1 down
}


case "$1" in
    start)
        interfaces_up && load_tc && load_iptables || exit 1
    ;;
    stop)
        flush_tc && flush_iptables && interfaces_down || exit 1
    ;;
    force-reload|restart)
        flush_tc && flush_iptables && interfaces_down && sleep 1
        interfaces_up && load_tc && load_iptables
    ;;   
    *)
        echo "Usage: $SCRIPTNAME {start|stop|restart}"
        exit 1
    ;;       
esac

exit 0

Nie jest to szczyt techniki ale działa, a to najważniejsze. Dodatkowo ten skrypt ma się odpalić przed skryptem sieciowym -- trzeba wyedytować nagłówek skryptu /etc/init.d/networking ale pierw usuńmy ten skrypt z autostartu:

# update-rc.d networking remove

Dopisujemy teraz imq w odpowiednich sekcjach:

### BEGIN INIT INFO
# Provides:          networking ifupdown
# Required-Start:    mountkernfs $local_fs urandom 
# Required-Stop:     $local_fs
# Should-Start:      peerblock iptables-persistent imq
# Should-Stop:       peerblock iptables-persistent imq
# Default-Start:     S
# Default-Stop:      0 6
# Short-Description: Raise network interfaces.
# Description:       Prepare /run/network directory, ifstate file and raise network interfaces, or take them down.
### END INIT INFO

I dodajemy oba skrypty do autostartu:

# update-rc.d networking defaults
# update-rc.d imq defaults

Reboot maszyny i powinno działać.

2. Jeśli nie IMQ, to co w takim razie?

Jeśli nie odpowiada nam za bardzo zabawa z kompilacją kernela i iptables, bo jak nie patrzeć jest to trochę upierdliwe, istnieje inne rozwiązanie, przy wdrażaniu którego za bardzo nie trzeba się wysilać. Umożliwia ono, co prawda, kontrolę tylko ruchu wychodzącego ale nie ma się większego wpływu na ruch przychodzący. Jednak ten sposób również potrafi sprawić, że nie zauważymy większej różnicy w użytkowaniu internetu przy odpalonym na full qbittorrencie, dlatego trzeba o nim parę słów napisać.

2.1. Kształtowanie ruchu wychodzącego przy pomocy -j CLASSIFY

Do kształtowania tego ruchu możemy wykorzystać iptables z celami -j MARK oraz -j CLASSIFY, mamy też możliwość skorzystania z cgroups, oraz z tc filter. Nie będę tutaj opisywał celu -j MARK, bo to już zostało zrobione przy okazji zabawy z interfejsami IMQ i zasadniczo reguły tam zastosowane nie różnią się zbytnio od tych, które by zostały użyte w tym przypadku, jedyne co, to trzeba by było pozmieniać interfejsy. W każdym razie w tym przypadku jest to wielce nieporęczne i nie ma sobie co tym głowy zawracać.

Cel -j CLASSIFY daje nam możliwość przekierowania ruchu do odpowiednich kolejek bez potrzeby zaprzęgania do tego tc filter -- od razu po przypasowaniu reguły w iptables, pakiet trafia do odpowiedniej kolejki. Poniżej jest przedstawiony prosty setup na ograniczenie ruchu wychodzącego z wykorzystaniem tego celu:

# tc qdisc del dev eth0 root 
# iptables -t mangle -F 2> /dev/null

# tc qdisc add dev eth0 root handle 1: htb default 4
# tc class add dev eth0 parent 1:0 classid 1:1 htb rate 950kbit ceil 950kbit 
# tc class add dev eth0 parent 1:1 classid 1:2 htb rate 450kbit ceil 950kbit prio 5
# tc class add dev eth0 parent 1:1 classid 1:3 htb rate 250kbit ceil 950kbit prio 0
# tc class add dev eth0 parent 1:1 classid 1:4 htb rate 250kbit ceil 950kbit prio 2

# iptables -t mangle -A POSTROUTING -o eth0 -m owner --gid-owner p2p -j CLASSIFY --set-class 1:2
# iptables -t mangle -A POSTROUTING -o eth0 -m owner --gid-owner p2p -j ACCEPT
# iptables -t mangle -A POSTROUTING -o eth0 -m owner --uid-owner morfik -j CLASSIFY --set-class 1:3
# iptables -t mangle -A POSTROUTING -o eth0 -m owner --gid-owner morfik -j ACCEPT
# iptables -t mangle -A POSTROUTING -o eth0 -m owner --uid-owner root -j CLASSIFY --set-class 1:3
# iptables -t mangle -A POSTROUTING -o eth0 -m owner --gid-owner root -j ACCEPT

Jakież to proste. xD Większość parametrów użytych w powyższym skrypcie zostały już opisane wcześniej. To co zasługuje na uwagę, to --set-class . Wartość tego parametru musi pasować do wartości classid w tc.

2.2. Kształtowanie ruchu wychodzącego przy pomocy cgroups

Jeśli używamy cgroups, możemy przy jego pomocy skierować ruch do odpowiednich kolejek. Samą instalację (i trochę konfiguracji) cgroups opisałem tutaj [13]. By kontrolować pakiety jakieś aplikacji, musimy dopisać odpowiednią linijkę z uwzględnieniem net_cls w /etc/cgrules.conf . Mój wpis od qbittorrenta wygląda następująco:

*:qbittorrent                cpu,net_cls             users/qbittorrent/
*:qbittorrent*              cpu,net_cls             users/qbittorrent/
*:qbittorrent-nox           cpu,net_cls             users/qbittorrent/
*:qbittorrent-nox*          cpu,net_cls             users/qbittorrent/

Uzupełniamy skrypt /etc/init.d/cgconfig o poniższe linijki:

mkdir -p $CGDIR/net_cls/users/qbittorrent
echo '1' > $CGDIR/net_cls/users/qbittorrent/cgroup.clone_children
echo '0x00010002' > $CGDIR/net_cls/users/qbittorrent/net_cls.classid

Ten numerek 0x00010002 , to są dwie liczby hexalne, składające się na określenie grupy. Ta ma numer w postaci 1:2. Cztery pierwsze cyfry odpowiadają liczbie przed ":" , cztery kolejne, cyfrze po ":". Tak więc mamy dwie liczby 0001 oraz 0002, co daje 1:2. Każda z pozycji może przyjąć 16 wartości, w końcu to zapisz szesnastkowy.

Ładujemy ponownie skrypt od konfiguracji oraz restartujemy demona cgrulesengd:

# /etc/init.d/cgrulesengd stop
# /etc/init.d/cgconfig restart
# /etc/init.d/cgrulesengd start

Tworzymy kolejki:

# tc qdisc del dev eth0 root 2> /dev/null
# iptables -t mangle -F 2> /dev/null

# tc qdisc add dev eth0 root handle 1: htb default 4
# tc class add dev eth0 parent 1:0 classid 1:1 htb rate 950kbit ceil 950kbit 
# tc class add dev eth0 parent 1:1 classid 1:2 htb rate 450kbit ceil 950kbit prio 5
# tc class add dev eth0 parent 1:1 classid 1:3 htb rate 250kbit ceil 950kbit prio 0
# tc class add dev eth0 parent 1:1 classid 1:4 htb rate 250kbit ceil 950kbit prio 2

Potrzebujemy jeszcze odpowiedniego przekierowania pakietów do klas w tc. Posłuży nam do tego celu tc filter:

# tc filter add dev eth0 protocol ip parent 1:0 prio 10 handle 1 cgroup

Nie potrzebujemy żadnych wpisów w iptables, wszystko jest zarządzane przez cgroups w w połączeniu z tc. W bmon wygląda to tak:

Interfaces                     | RX bps       pps     %| TX bps       pps     %
  lo                           |    271B        1      |    271B        1
->eth0                         |   5.99KiB     62      | 117.03KiB    119
    qdisc 1: (htb)             |      0         0      | 117.03KiB    119
      class 1:1 (htb)          |      0         0      | 116.99KiB    118   101%
        class 1:2 (htb)        |      0         0      | 116.92KiB    117   213%
        class 1:3 (htb)        |      0         0      |      0         0    0%
        class 1:4 (htb)        |      0         0      |     63B        0    0%
      cls :1 (cgroup)          |      0         0      |      0         0

U mnie się pojawiały dziwne problemy przy wykorzystaniu cgroups -- albo nie brało pakietów do odpowiednich grup, mimo, że id kolejki był sprecyzowany w pliku net_cls.classid, albo po pewnym czasie ruch się rozdzielał na kolejkę, do której iść powinien i kolejkę domyślną. W przypadku gdy ruch w ogóle nie szedł do przeznaczonej kolejki, można było przy pomocy echo jeszcze raz zapisać plik net_cls.classid. Nie wiem czemu tak się dzieje, może to tylko u mnie. W każdym razie cele -j MARK i -j CLASSIFY działają bez zarzutu, także jeśli mamy jakieś problemy z cgroups, zawsze możemy z tych targetów skorzystać i przekierować ruch przy pomocy iptables.

3. Interfejsy IFB

Jeśli jednak interesuje nas kontrola ruchu w obie strony ale nie mamy zamiaru przy tym kompilować kernela i iptables z łatami IMQ, możemy skorzystać z natywnego rozwiązania oferowanego przez kernel, czyli interfejsów IFB. Działają na podobnej zasadzie co interfejsy IMQ, również będą dwa, z których jeden będzie łapał i kształtował ruch wychodzący, a drugi ruch skierowany do naszej maszyny. W tym przypadku nie da rady kształtować ruchu przychodzącego przy pomocy iptables. Trzeba do tego celu użyć tc filter. Nie wiem czy da radę nim złapać ruch na qbittorrencie, bo składnia tc filter jest trochę skompilowana ale bez problemu idzie wyłapać wszystko inne.

By móc operować na interfejsach IFB i przy ich pomocy rozgraniczyć pakiety wychodzące od przychodzących, musimy załadować kilka modułów. Dopisujemy zatem do pliku /etc/modules poniższe linijki:

ifb numifbs=2
sch_fq_codel
act_mirred

Moduły z /etc/modules są ładowane na starcie systemu. Jeśli chcemy załadować jakiś moduł w systemie ręcznie, musimy użyć do tego celu modprobe .

Tworzymy kolejki:

# tc qdisc del dev eth0 root 
# tc qdisc del dev eth0 ingress
# tc qdisc del dev ifb0 root
# tc qdisc del dev ifb1 root

# iptables -t mangle -F 2> /dev/null
# iptables -t mangle -X IFB-OUT 2> /dev/null

# ip link set ifb0 up
# ip link set ifb1 up

# tc qdisc add dev eth0 parent root handle 1:0 htb
# tc filter add dev eth0 parent 1:0 protocol ip prio 10 u32 match ip dst 0.0.0.0/0 flowid 1:1 action mirred egress redirect dev ifb0
# tc qdisc add dev eth0 handle ffff: ingress
# tc filter add dev eth0 parent ffff: protocol ip prio 10 u32 match ip src 0.0.0.0/0 flowid 2:1 action mirred egress redirect dev ifb1

# tc qdisc add dev ifb0 root handle 1: htb default 4
# tc class add dev ifb0 parent 1:0 classid 1:1 htb rate 950kbit ceil 950kbit 
# tc class add dev ifb0 parent 1:1 classid 1:2 htb rate 250kbit ceil 950kbit prio 5
# tc class add dev ifb0 parent 1:1 classid 1:3 htb rate 450kbit ceil 950kbit prio 0
# tc class add dev ifb0 parent 1:1 classid 1:4 htb rate 250kbit ceil 950kbit prio 2

# tc qdisc add dev ifb1 root handle 2: htb default 4
# tc class add dev ifb1 parent 2:0 classid 2:1 htb rate 9500kbit ceil 9500kbit
# tc class add dev ifb1 parent 2:1 classid 2:2 htb rate 2500kbit ceil 9500kbit prio 2
# tc class add dev ifb1 parent 2:1 classid 2:3 htb rate 4500kbit ceil 9500kbit prio 0
# tc class add dev ifb1 parent 2:1 classid 2:4 htb rate 2500kbit ceil 9500kbit prio 5

Pakietami wyjściowymi możemy sterować tak jak w poprzednich przypadkach, czyli przez iptables z celami -j MARK oraz -j CLASSIFY i cgroups. By kontrolować ruch wychodzący w tym przypadku, stworzyłem sobie kilka regułek z targetem -j CLASSIFY :

# iptables -t mangle -A POSTROUTING -m owner --gid-owner p2p -j CLASSIFY --set-class 1:2
# iptables -t mangle -A POSTROUTING -m owner --gid-owner p2p -j ACCEPT
# iptables -t mangle -A POSTROUTING -m owner --uid-owner morfik -j CLASSIFY --set-class 1:3
# iptables -t mangle -A POSTROUTING -m owner --uid-owner morfik -j ACCEPT
# iptables -t mangle -A POSTROUTING -m owner --uid-owner root -j CLASSIFY --set-class 1:3
# iptables -t mangle -A POSTROUTING -m owner --uid-owner root -j ACCEPT

I to w zasadzie działa, przynajmniej jeśli chodzi o ruch wychodzący.

Teraz kolej na ruch skierowany do naszej maszyny. Jeśli chcemy dać wyższy priorytet dla stron www, to musimy napisać odpowiednie regułki dla tc filter uwzględniające porty źródłowe 80 i 443, po czym przekierować je do kolejki z niższym numerem prio. Najprościej to można zrobić tak:

# tc filter add dev ifb1 parent 2: protocol ip prio 10 u32 match ip sport 443 0xffff classid 2:3
# tc filter add dev ifb1 parent 2: protocol ip prio 10 u32 match ip sport 80 0xffff classid 2:3

Teraz odpalmy firefoxa i wchodzimy na jakąś stronę www. U mnie bmon pokazuje poniższy wynik:

Interfaces                     | RX bps       pps     %| TX bps       pps     %
->lo                           |    683B       10      |    683B       10
  eth0                         |  82.31KiB    140      |  74.95KiB    153
    qdisc 1: (htb)             |      0         0      |  74.94KiB    153
      cls none (u32)           |      0         0      |      0         0
      cls 8000: (u32)          |      0         0      |      0         0
      cls 8000:800 (u32)       |      0         0      |      0         0
    qdisc ffff: (ingress)      |      0         0      |  80.39KiB    140
      cls none (u32)           |      0         0      |      0         0
      cls 8000: (u32)          |      0         0      |      0         0
      cls 8000:800 (u32)       |      0         0      |      0         0
  ifb0                         |  74.94KiB    153      |  74.94KiB    153
    qdisc 1: (htb)             |      0         0      |  74.94KiB    153
      class 1:1 (htb)          |      0         0      |  74.89KiB    153   65%
        class 1:2 (htb)        |      0         0      |  67.92KiB     98   223%
        class 1:3 (htb)        |      0         0      |   6.81KiB     51   12%
        class 1:4 (htb)        |      0         0      |    165B        2    1%
  ifb1                         | 239.97KiB    241      | 239.97KiB    241
    qdisc 2: (htb)             |      0         0      | 239.97KiB    241
      class 2:1 (htb)          |      0         0      | 241.08KiB    242   21%
        class 2:2 (htb)        |      0         0      |      0         0    0%
        class 2:3 (htb)        |      0         0      | 236.88KiB    185   43%
        class 2:4 (htb)        |      0         0      |   4.20KiB     56    1%
      cls none (u32)           |      0         0      |      0         0
      cls 8000: (u32)          |      0         0      |      0         0
      cls 8000:800 (u32)       |      0         0      |      0         0
      cls 8000:801 (u32)       |      0         0      |      0         0

Ten 0xffff jest to maska dla portów, podobna do tej dla adresów ip. W przypadku ustawienia ffff, oznacza to tylko jeden port. Można oczywiście ustawić zakres portów przez zmianę tej maski ale jest to trochę upierdliwe i ja tego raczej w pamięci nie policzę. W zrozumieniu składni u32 mogą przydatne okazać się te linki: man ematch oraz man u32 filter [14].

Jak widać jest to trochę skomplikowane ale dla naszych potrzeb spokojnie wystarczą te cztery parametry: src, dst, sport i dport. By sprecyzować adres ip zamiast portu, wstawiamy dst 222.28.1.40/32 w miejsce sport 80 0xffff. Reguła będzie odnosić się do adresu docelowego 222.28.1.40. Bu ustawić zakres adresów, wystarczy zmienić maskę, podobnie jak w przypadku portów.

Jeśli jednak chcielibyśmy się pobawić w coś bardziej zaawansowanego z wykorzystaniem selektora u32 (i innych) warto nauczyć się przeliczać liczy hex, binarne i decymalne. Poniżej jest kilka przykładów.

Zamiana hex na dec:

$ echo "obase=10;ibase=16;C0A80100" | bc
3232235776

Zamiana hex na bi

$ echo "obase=2;ibase=16;C0A80100" | bc
11000000101010000000000100000000

Zamiana bi na dec:

$ echo "obase=10;ibase=2;11000000101010000000000100000000" | bc
3232235776

I tak dalej. Kluczowe to odpowiednio dobrać obase oraz ibase, które definiują format liczb -- ibase to format wejściowy, a obase wyjściowy.

Warto też się zaznajomić z wyglądem nagłówków tcp i ip [15].

Podobnie jak w przypadku interfejsów IMQ, możemy sobie zrobić skrypt startowy z użytymi powyżej regułami. Mój skrypt prezentuje się tak:

### BEGIN INIT INFO
# Provides:          ifb
# Required-Start:    mountkernfs $local_fs
# Required-Stop:     $local_fs
# Should-Start:      
# Should-Stop:       
# Default-Start:     S
# Default-Stop:      
# X-Start-Before:    
# X-Stop-After:      
# Short-Description: tc
# Description:       tc configuration for ifb interfaces
### END INIT INFO

SCRIPTNAME="ifb"

load_tc()
{
    echo -n "Loading tc configuration... "
    tc qdisc add dev eth0 parent root handle 1:0 htb
    tc filter add dev eth0 parent 1:0 protocol ip prio 10 u32 match ip dst 0.0.0.0/0 flowid 1:1 action mirred egress redirect dev ifb0
    tc qdisc add dev eth0 handle ffff: ingress
    tc filter add dev eth0 parent ffff: protocol ip prio 10 u32 match ip src 0.0.0.0/0 flowid 2:1 action mirred egress redirect dev ifb1

    tc qdisc add dev ifb0 root handle 1: htb default 5
    tc class add dev ifb0 parent 1:0 classid 1:1 htb rate 950kbit ceil 950kbit 
    tc class add dev ifb0 parent 1:1 classid 1:2 htb rate 150kbit ceil 950kbit prio 5
    tc class add dev ifb0 parent 1:1 classid 1:3 htb rate 200kbit ceil 950kbit prio 4
    tc class add dev ifb0 parent 1:1 classid 1:4 htb rate 450kbit ceil 950kbit prio 0
    tc class add dev ifb0 parent 1:1 classid 1:5 htb rate 150kbit ceil 950kbit prio 2

    tc qdisc add dev ifb1 root handle 2: htb default 5
    tc class add dev ifb1 parent 2:0 classid 2:1 htb rate 9500kbit ceil 9500kbit
    tc class add dev ifb1 parent 2:1 classid 2:2 htb rate 1500kbit ceil 9500kbit prio 2
    tc class add dev ifb1 parent 2:1 classid 2:3 htb rate 2000kbit ceil 9500kbit prio 4
    tc class add dev ifb1 parent 2:1 classid 2:4 htb rate 4500kbit ceil 9500kbit prio 0
    tc class add dev ifb1 parent 2:1 classid 2:5 htb rate 1500kbit ceil 9500kbit prio 5
    
    tc filter add dev ifb1 parent 2: protocol ip prio 7 u32 match ip sport 80 0xffff classid 2:3
    tc filter add dev ifb1 parent 2: protocol ip prio 8 u32 match ip sport 443 0xffff classid 2:3
    tc filter add dev ifb1 parent 2: protocol ip prio 20 u32 match ip sport 993 0xffff classid 2:2
    tc filter add dev ifb1 parent 2: protocol ip prio 21 u32 match ip sport 143 0xffff classid 2:2
    tc filter add dev ifb1 parent 2: protocol ip prio 22 u32 match ip sport 465 0xffff classid 2:2
    tc filter add dev ifb1 parent 2: protocol ip prio 35 u32 match ip sport 5222 0xffff classid 2:2
    tc filter add dev ifb1 parent 2: protocol ip prio 36 u32 match ip sport 5223 0xffff classid 2:2
    echo "done!"
}

load_iptables()
{   
    echo -n "Loading iptables configuration... "
    iptables -t mangle -A POSTROUTING -o eth0 -m length --length 40:68 -j CLASSIFY --set-class 1:3
    iptables -t mangle -A POSTROUTING -o eth0 -m length --length 40:68 -j ACCEPT
    iptables -t mangle -A POSTROUTING -o eth0 -m owner --gid-owner p2p -j CLASSIFY --set-class 1:2
    iptables -t mangle -A POSTROUTING -o eth0 -m owner --gid-owner p2p -j ACCEPT
    iptables -t mangle -A POSTROUTING -o eth0 -m owner --uid-owner morfik -j CLASSIFY --set-class 1:4
    iptables -t mangle -A POSTROUTING -o eth0 -m owner --uid-owner morfik -j ACCEPT
    iptables -t mangle -A POSTROUTING -o eth0 -m owner --uid-owner root -j CLASSIFY --set-class 1:4
    iptables -t mangle -A POSTROUTING -o eth0 -m owner --uid-owner root -j ACCEPT
    echo "done!"
}

interfaces_up()
{
    ip link set ifb0 up
    ip link set ifb1 up
}

flush_tc()
{
    echo -n "Removing tc qdiscs... "
    tc qdisc del dev eth0 root 
    tc qdisc del dev eth0 ingress
    tc qdisc del dev ifb0 root
    tc qdisc del dev ifb1 root
    echo "done!"
}

flush_iptables()
{
    echo -n "Flushing mangle table... "
    iptables -t mangle -F 2> /dev/null
    echo "done!"
}

interfaces_down()
{
    ip link set ifb0 down
    ip link set ifb1 down
}

case "$1" in
    start)
        interfaces_up && load_tc && load_iptables || exit 1
    ;;
    stop)
        flush_tc && flush_iptables && interfaces_down || exit 1
    ;;
    force-reload|restart)
        flush_tc && flush_iptables && interfaces_down && sleep 1
        interfaces_up && load_tc && load_iptables
    ;;   
    *)
        echo "Usage: $SCRIPTNAME {start|stop|restart}"
        exit 1
    ;;       
esac

exit 0

Edytujemy jeszcze nagłówki skryptu /etc/init.d/networking ale pierw musimy go usunąć z autostartu:

# update-rc.d networking remove

Teraz dodajemy w odpowiednie sekcje ifb:

### BEGIN INIT INFO
# Provides:          networking ifupdown
# Required-Start:    mountkernfs $local_fs urandom 
# Required-Stop:     $local_fs
# Should-Start:      peerblock iptables-persistent ifb
# Should-Stop:       peerblock iptables-persistent ifb
# Default-Start:     S
# Default-Stop:      0 6
# Short-Description: Raise network interfaces.
# Description:       Prepare /run/network directory, ifstate file and raise network interfaces, or take them down.
### END INIT INFO

Dodajemy oba skrypty do autostartu:

# update-rc.d networking defaults
# update-rc.d ifb defaults

4. Statystyki ruchu

Istnieje kilka narzędzi, które pokazują rozpiskę aktualnej konfiguracji traffic control.

Inforamcje o qdisc:

# tc -d -s qdisc show dev eth0
qdisc htb 1: root refcnt 2 r2q 10 default 0 direct_packets_stat 1044925 ver 3.17 direct_qlen 1000
 Sent 161806806 bytes 1065008 pkt (dropped 11997, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0
qdisc ingress ffff: parent ffff:fff1 ----------------
 Sent 1280866443 bytes 1058357 pkt (dropped 7425, overlimits 0 requeues 0)
 backlog 0b 0p requeues 0
# tc -d -s qdisc show dev ifb0
qdisc htb 1: root refcnt 2 r2q 10 default 5 direct_packets_stat 0 ver 3.17 direct_qlen 32
 Sent 163118498 bytes 1074893 pkt (dropped 12069, overlimits 2223331 requeues 0)
 backlog 0b 28p requeues 0
# tc -d -s qdisc show dev ifb1
qdisc htb 2: root refcnt 2 r2q 10 default 5 direct_packets_stat 0 ver 3.17 direct_qlen 32
 Sent 1302094010 bytes 1057023 pkt (dropped 7561, overlimits 1957564 requeues 0)
 backlog 0b 29p requeues 0

Informacje o filtrach:

tc -s -d -p filter show dev ifb1
filter parent 2: protocol ip pref 7 u32
filter parent 2: protocol ip pref 7 u32 fh 800: ht divisor 1
filter parent 2: protocol ip pref 7 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid 2:3  (rule hit 1388968 success 6829)
  match sport 80 (success 6829 )
filter parent 2: protocol ip pref 8 u32
filter parent 2: protocol ip pref 8 u32 fh 801: ht divisor 1
filter parent 2: protocol ip pref 8 u32 fh 801::800 order 2048 key ht 801 bkt 0 flowid 2:3  (rule hit 1382139 success 5130)
  match sport 443 (success 5130 )
filter parent 2: protocol ip pref 20 u32
filter parent 2: protocol ip pref 20 u32 fh 802: ht divisor 1
filter parent 2: protocol ip pref 20 u32 fh 802::800 order 2048 key ht 802 bkt 0 flowid 2:2  (rule hit 1377009 success 1940)
  match sport 993 (success 1940 )
filter parent 2: protocol ip pref 21 u32
filter parent 2: protocol ip pref 21 u32 fh 803: ht divisor 1
filter parent 2: protocol ip pref 21 u32 fh 803::800 order 2048 key ht 803 bkt 0 flowid 2:2  (rule hit 1375069 success 105)
  match sport 143 (success 105 )
filter parent 2: protocol ip pref 22 u32
filter parent 2: protocol ip pref 22 u32 fh 804: ht divisor 1
filter parent 2: protocol ip pref 22 u32 fh 804::800 order 2048 key ht 804 bkt 0 flowid 2:2  (rule hit 1374964 success 0)
  match sport 465 (success 0 )
filter parent 2: protocol ip pref 35 u32
filter parent 2: protocol ip pref 35 u32 fh 805: ht divisor 1
filter parent 2: protocol ip pref 35 u32 fh 805::800 order 2048 key ht 805 bkt 0 flowid 2:2  (rule hit 1374964 success 111)
  match sport 5222 (success 111 )
filter parent 2: protocol ip pref 36 u32
filter parent 2: protocol ip pref 36 u32 fh 806: ht divisor 1
filter parent 2: protocol ip pref 36 u32 fh 806::800 order 2048 key ht 806 bkt 0 flowid 2:2  (rule hit 1374853 success 113)
  match sport 5223 (success 113 )

root:~# tc -s -d -p filter show dev eth0
filter parent 1: protocol ip pref 10 u32
filter parent 1: protocol ip pref 10 u32 fh 800: ht divisor 1
filter parent 1: protocol ip pref 10 u32 fh 800::800 order 2048 key ht 800 bkt 0 flowid 1:1  (rule hit 2787825 success 2787825)
  match IP dst 0.0.0.0/0 (success 2787825 )
        action order 1: mirred (Egress Redirect to device ifb0) stolen
        index 1 ref 1 bind 1 installed 1852 sec
        Action statistics:
        Sent 212247685 bytes 1425206 pkt (dropped 0, overlimits 14476 requeues 0)
        backlog 0b 0p requeues 0

Jeśli komuś niezbyt odpowiada zapis tekstowy i wolałby zobaczyć rozpiskę w postaci jakiegoś wykresu czy coś, to istnieje narzędzie, które na podstawie tych literek i cyferek zrobi nam miły dla oka obrazek. Sam programik znalazłem na tym blogu, a jego kod jest dostępny pod tym adresem [16].

Program wywołujemy przez:

$ ./tcviz.py eth0 | dot -Tpng > tc.png

5. Jak odpalać qbittorrenta?

Przez parę dni zastanawiałem się jak ogarnąć uruchamianie qbittorrenta, bo pakiety trzeba przecie łapać albo po uid albo po gid. Propozycji było kilka. Pierwsza z nich zakładała wykorzystanie sudo ale w tym przypadku jest trochę za dużo roboty -- trzeba pilnować uprawnień plików, trzeba przenosić konfigurację i tworzyć nowego użytkownika, bawić się aclem. Niby nic wielkiego ale istnieje inny, dużo prostszy sposób, który wykorzystuje grupy. Normalnie proces jest odpalany z prawami użytkownika, który ten proces uruchamia. Dodatkowo grupa główna tego użytkownika jest brana pod uwagę przy uruchamianiu procesu, w efekcie czego proces jest uruchomiony jako użytkownik morfik i grupa również morfik. Ale przecie grupy nie zostały stworzone po to by używać cały czas tylko jednej z nich, a biorąc pod uwagę fakt, że pakiety możemy filtrować również po grupie, uruchomimy proces z inną grupą.

Tworzymy zatem nową grupę p2p i dodajemy swojego użytkownika do niej:

# groupadd -g 5004 p2p
# adduser morfik p2p

By móc bez hasła używać id innej grupy musimy być podpięci pod tę grupę, można to sprawdzić wklepując do terminala id.

By odpalić proces z inną grupą, wydajemy poniższe polecenie:

$ nice -n 19 ionice -c 2 -n 7 sg p2p -c qbittorrent

Kluczowe w powyższej linijce jest sg. To za jego pomocą ustawiamy procesowi odpowiednią grupę. Dodatkowo, jest jeszcze zaprzęgnięty do pracy nice oraz ionice -- mają na celu odciążenie procesora i dysku w przypadku gdyby inne procesy dość gwałtownie wykorzystywały zasoby maszyny.

QBittorrent posiada klienta nox, czyli coś co działa bez potrzeby odpalania X-ów, a zarządzać nim można przez www. Zjada 2-3 krotnie mniej zasobów niż jego graficzny odpowiednik, co powinno ucieszyć ludzi, którzy... mają mniej RAMu niż ja. xD Posiada on także skrypt startowy, choć domyślnie nie jest dołączony w pakiecie. Wykorzystuje on do uruchomienia qbittorrenta start-stop-daemon -- standardowy mechanizm odpalania procesów startowych w debianie i można przy jego pomocy także sprecyzować grupę. Skrypt prezentuje się tak:

#! /bin/sh
### BEGIN INIT INFO
# Provides:          qbittorrent-nox
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Should-Start:      iptables-persistent pgl
# Should-Stop:       iptables-persistent pgl
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Starts QBittorrent
# Description:       Start qbittorrent-nox on start. Change USER= before running
### END INIT INFO

# Author: Jesper Smith
#

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="QBittorrent"
NAME=qbittorrent-nox
DAEMON=/usr/bin/$NAME
DAEMON_ARGS=""
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/qbittorrent-nox
USER=morfik:p2p

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

if [ "$START" = "no" ]; then
    echo "Autostart wyłączony -- zobacz /etc/default/$NAME ."
    exit 0
fi
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions

#
# Function that starts the daemon/service
#
do_start()
{
    # Return
    #   0 if daemon has been started
    #   1 if daemon was already running
    #   2 if daemon could not be started
    start-stop-daemon -c $USER -b -t --start --quiet  --exec $DAEMON  \
                || return 1

    start-stop-daemon -c $USER -b --start --quiet --exec $DAEMON -- \
        $DAEMON_ARGS \
        || return 2
    sleep 1
}

#
# Function that stops the daemon/service
#
do_stop()
{
    start-stop-daemon -c $USER --quiet  --stop --exec $DAEMON
    sleep 2
    return "$?"
}

VERBOSE="yes"

case "$1" in
  start)
    [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
    do_start
    case "$?" in
        0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
        2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
    ;;
  stop)
    [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
    do_stop
    case "$?" in
        0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
        2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
    ;;
  status)
       status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
       ;;
  restart|force-reload)
    log_daemon_msg "Restarting $DESC" "$NAME"
    do_stop
    case "$?" in
      0|1)
        do_start
        case "$?" in
            0) log_end_msg 0 ;;
            1) log_end_msg 1 ;; # Old process is still running
            *) log_end_msg 1 ;; # Failed to start
        esac
        ;;
      *)
        # Failed to stop
        log_end_msg 1
        ;;
    esac
    ;;
  *)
    #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
    echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
    exit 3
    ;;
esac

Jedyne co musimy dostosować to linijkę z USER=morfik:p2p. Dodajemy skrypt do autostartu przy pomocy:

# update-rc.d qbittorrent-nox defaults

I będzie sobie działał w tle cały czas od chwili załadowania się systemu.

Więcej informacji można znaleźć w poniższych linkach:

https://wiki.gentoo.org/wiki/Traffic_shaping [17]
https://wiki.archlinux.org/index.php/Advanced_Traffic_Control [18]
http://lartc.org/lartc.html#LARTC.QDISC [19]
http://bromirski.net/docs/translations/lartc-pl.html#LARTC.QDISC [20] (PL)
http://wampir.mroczna-zaloga.org/archives/21-o-ksztaltowaniu-ruchu.html [21]
http://robert.nowotniak.com/en/security/htb/ [22]
https://www.kernel.org/doc/Documentation/cgroups/net_cls.txt [23]
http://edseek.com/~jasonb/articles/traffic_shaping/index.html [24]
http://manpages.debian.net/cgi-bin/man.cgi?query=tc&sektion=8&apropos=0&manpath=Debian+7.0+wheezy&locale= [25]
http://www.tamos.net/~rhay/wp/overhead/overhead.htm [26]
http://linux-ip.net/articles/Traffic-Control-HOWTO/intro.html [27]


Przypisy:

  1. #1
  2. #1.1
  3. #2
  4. #2.1
  5. #2.2
  6. #3
  7. #4
  8. #5
  9. http://www.linuximq.net/patches.html
  10. https://www.kernel.org/
  11. https://www.kernel.org/category/signatures.html
  12. http://manpages.debian.net/cgi-bin/man.cgi?query=tc&sektion=8&apropos=0&manpath=Debian+7.0+wheezy&locale=
  13. http://dug.net.pl/tekst/269
  14. http://manpages.debian.net/cgi-bin/man.cgi?query=ematch&apropos=0&sektion=0&manpath=Debian+testing+jessie&format=html&locale=en
  15. http://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_segment_structure
  16. http://ze.phyr.us/visualizing-linux-traffic-control-setup/
  17. https://wiki.gentoo.org/wiki/Traffic_shaping
  18. https://wiki.archlinux.org/index.php/Advanced_Traffic_Control
  19. http://lartc.org/lartc.html#LARTC.QDISC
  20. http://bromirski.net/docs/translations/lartc-pl.html#LARTC.QDISC
  21. http://wampir.mroczna-zaloga.org/archives/21-o-ksztaltowaniu-ruchu.html
  22. http://robert.nowotniak.com/en/security/htb/
  23. https://www.kernel.org/doc/Documentation/cgroups/net_cls.txt
  24. http://edseek.com/~jasonb/articles/traffic_shaping/index.html
  25. http://manpages.debian.net/cgi-bin/man.cgi?query=tc&sektion=8&apropos=0&manpath=Debian+7.0+wheezy&locale=
  26. http://www.tamos.net/~rhay/wp/overhead/overhead.htm
  27. http://linux-ip.net/articles/Traffic-Control-HOWTO/intro.html