Улучшаем интернет
или что можно покрутить, чтобы было лучше и удобнее

Есть домашний компьютер (ноутбук). Есть интернет. Есть выделенная линия. Не всегда все работает хорошо. Хочется, чтобы это не сильно влияло на удобство пользования. Хочется немного сэкономить на трафике. Хочется убедиться, что нас не обманывают и счета выставляют правильно. Какие есть утилиты и как ими воспользоваться? На примере (K)Ubuntu 8.04.

Файервол

На нашем компьютере могут быть запущены сервисы, доступные из сети. Нужно закрыть к ним доступ, чтобы быть уверенными, что никто извне не сможет получить доступ к этим сервисам и совершить что-нибудь нехорошее. Сделать это можно с помощью штатного фильтра пакетов Linux - iptables.

Создадим (с правами rootа) файл /etc/init.d/firewall следующего содержания:

#!/bin/sh

IPTABLES=/sbin/iptables

# очищаем все цепочки,
# чтобы этот скрипт можно было бы просто запускать, без перезагрузки :)
$IPTABLES -F INPUT
$IPTABLES -F OUTPUT
$IPTABLES -F FORWARD

# по умолчанию все входящие пакеты - уничтожаем
$IPTABLES -P INPUT DROP
# пакеты с локального интерфейса - принимаем
# (локальные службы могут обмениваться данными)
$IPTABLES -A INPUT -i lo -j ACCEPT
# принимаем пакеты установленных соединений
# (входящий трафик уже установленных нами соединений нужно принять)
$IPTABLES -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# возможно, здесь понадобится разрешить входящие соединения для специальных случаев
# например, входящие GRE пакеты с определенного адреса для pptp VPN подключений
#$IPTABLES -A INPUT -p gre -s peer.vnp.net -j ACCEPT
# или для работы UPnP на локальном маршрутизаторе
#$IPTABLES -A INPUT -s 192.168.0.1 -p udp --sport 1900 -j ACCEPT
# про остальные пакеты (перед уничтожением) помещаем запись в журнал
$IPTABLES -A INPUT -m limit --limit 1/minute -j LOG --log-prefix "IPT INPUT dropped "

# все исходящие пакеты - разрешаем
$IPTABLES -P OUTPUT ACCEPT
# эти пакеты порождаются процессами на нашем компьютере
# можно разрешить здесь пакеты только на известные порты или только
# от определенного пользователя, но будем считать,
# что зловредных программ (и пользователей) у нас нет

# все пакеты, пересылаемые с интерфейса на интерфейс, - уничтожаем
# пока наш компьютер не работает маршрутизатором, разрешать нет смысла
$IPTABLES -P FORWARD DROP
# перед уничтожением помещаем запись в журнал
$IPTABLES -A FORWARD -m limit --limit 1/minute -j LOG --log-prefix "IPT FORWARD dropped "

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

sudo ln -s /etc/init.d/firewall /etc/rc2.d/S99firewall

Подробности смотрите в man iptables.

Прокси

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

Squid слишком тяжел... Воспользуемся Polipo - кеширующим прокси для персонального использования.

Устанавливаем пакет polipo. Конфигурация по умолчанию вполне работоспособна. Я лишь добавил в файл /etc/polipo/config одну строчку:

tunnelAllowedPorts = 22, 80, 443, 2096
Чтобы разрешить HTTPS через прокси на эти порты (2096 - ну совершенно нестандартный).

Документация по Polipo доступна в нем самом: зайдите браузером на http://localhost:8123/.

Теперь надо настроить браузеры для работы с прокси. Для большей гибкости я создал скрипт автоматической конфигурации прокси. Это Javascript файл, который выполняется браузером для того, чтобы определить, какой прокси необходимо использовать (и надо ли использовать вообще) для обращения к данному URL.

Расположение скрипта указывается в настройках браузера. А сам скрипт выглядит так:

//(C) Oskar Pearson and the Internet Solution (http://www.is.co.za)

// Эта функция и вызывается браузером.
// Она принимает два параметра - полный URL ресурса и имя хоста.
// Множественные вспомогательные функции, используемые внутри,
// определены где-то "внутри" браузера, работают :)
// Функция возвращает значения вида:
// DIRECT - использовать прямое подключение, без прокси
// PROXY hostname:port - адрес и порт прокси
// разделенные точкой с запятой.
// Таким образом, можно использовать несколько прокси, последующие
// будут использоваться при недоступности предыдущих.

function FindProxyForURL(url, host) {
    // Если имя хоста не содержит доменной части, не используем прокси
    if (isPlainHostName(host))
        return "DIRECT";

    // Эти правила задают прямое подключение к машинам
    // в локальной сети и локальной машине. Имя хоста
    // совпадает с соответствующей маской.
    if (shExpMatch(host, "intranet*") ||
        shExpMatch(host, "internal*") ||
        shExpMatch(host, "localhost*") ||
        shExpMatch(host, "*localdomain") ||
        shExpMatch(host, "192.168.*") ||
        shExpMatch(host, "10.*"))
        return "DIRECT";

    // Если доменное имя не резолвится, тоже пробуем без прокси.
    if (!isResolvable(host))
        return "DIRECT";

    // Различные большие файлы (которые все равно не HTML страницы)
    // тоже получаем напрямую.
    if (shExpMatch(url, "*.(rar|zip|mp3|ogg|avi|tar|tar.gz)"))
        return "DIRECT";

    // Через прокси пойдет только HTTP и HTTPS трафик.
    if (url.substring(0, 5) == "http:" ||
        url.substring(0, 6) == "https:"
        //url.substring(0, 4) == "ftp:" ||
        //url.substring(0, 7) == "gopher:"
       )
    // Вот и адрес прокси.
    // Если прокси недоступен, идем напрямую.
        return "PROXY localhost:8123; DIRECT";

    return "DIRECT";
}
В Википедии можно почитать про автоконфигурирование прокси (на английском).

Кэширование DNS

В случае неустойчивого соединения с интернетом бывает так, что качать и серфить более менее можно, но все стопорится из-за DNS. При неустойчивой связи разрешение доменных имен может происходить довольно долго и не всегда с первого раза. Помогает локальный кэширующий DNS сервер. К тому же это позволит чуть-чуть сэкономить на трафике :)

Proxy DNS Server (пакет pdnsd) как раз и создан для наших целей. Он кэширует запросы и хранит кэш на диске.

По умолчанию файл конфигурации /etc/pdnsd.conf игнорируется. Вместо него используется один из файлов из каталога /usr/share/pdnsd/, что задается параметром AUTO_MODE в файле /etc/default/pdnsd. Для ручной настройки этот параметр нужно закомментировать.

Далее правим /etc/pdnsd.conf. Я лишь добавил секцию с адресами DNS серверов моего провайдера:

server {
//  метка для группы серверов
    label="dom.ru";
//  IP адреса серверов
    ip="91.144.168.1";
    ip="91.144.170.1";
//  доступность серверов проверяется посылкой пустого DNS запроса
    uptest=query;
//  таймаут для ответов сервера, 30 секунд
    timeout=30;
//  доступность серверов проверяется каждые 60 секунд
    interval=60;
}

В /etc/resolv.conf я оставил лишь одну строчку:

nameserver 127.0.0.1
Практика показала, что не стоит сюда также вписывать адреса DNS-серверов провайдера. Если сервера недоступны, pdnsd будет возвращать либо закэшированный ответ, либо - немедленно - ошибку. В случае ошибки приложения будут затем запрашивать сервера провайдера и ждать положенный таймаут, задержка не уменьшится. Если же приложения обращаются только к pdnsd, ответ, как было сказано выше, будет получен немедленно. И таймаут можно уменьшить, настроив соответствующий параметр. И доступность серверов будет проверять pdnsd, а не каждое приложение.

Проконтролировать работу и поуправлять нашим сервером можно с помощью команды pdnsd-ctl, например,

sudo pdnsd-ctl status
или
sudo pdnsd-ctl server dom.ru retest

Приоритезация трафика

На анлимитных тарифах доступа к интернету, ширина канала, как правило, ограничена. Возникает такая проблема. Какое-то приложение занимается скачиванием данных и занимает всю полосу канала. Но одновременно хочется просто ходить по страничкам браузером. А канал уже занят. В результате страницы открываются неоправданно медленно, что очень некомфортно.

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

Исследование (чтение man tc, man tc-pfifo_fast и man tc-prio) показало, что Linux с настройками по умолчанию вполне умеет приоритизировать трафик по значению поля TOS IP пакетов. Имеется три очереди для пакетов с разным приоритетом, пакеты из очереди с меньшим приоритетом начинают обрабатываться только если в очереди с большим приоритетом нет пакетов для обработки. С настройками по умолчанию приоритеты разпределяются так (не вдаваясь сильно в подробности):

  1. Наивысший приоритет - пакеты, у которых в поле TOS установлен бит Minimize-Delay.
  2. Средний приоритет - пакеты, у которых в поле TOS никакие биты не установлены. Таковы, как правило, пакеты, создаваемые обычными приложениями.
  3. Низший приоритет - пакеты, у которых в поле TOS установлен бит Maximize-Throughput.

Поле же TOS можно установить, на основе разнообразных критериев, с помощью тех же iptables. В результате файервол дополняется следующими строками:

# очищаем правила
$IPTABLES -t mangle -F INPUT
$IPTABLES -t mangle -F OUTPUT

# модуль connbytes позволяет учитывать объем трафика,
# переданного через соединение
# в данном случае учитывается трафик в байтах, переданный в обе стороны

# первые 50 Кбайт (новые соединения) имеют максимальный приоритет
$IPTABLES -t mangle -A INPUT \
    -m connbytes --connbytes 0:51200 --connbytes-dir both --connbytes-mode bytes \
    -j TOS --set-tos Minimize-Delay
$IPTABLES -t mangle -A OUTPUT \
    -m connbytes --connbytes 0:51200 --connbytes-dir both --connbytes-mode bytes \
    -j TOS --set-tos Minimize-Delay

# если передано более 200 Кбайт (старые соединения), - минимальный приоритет
$IPTABLES -t mangle -A INPUT \
    -m connbytes --connbytes 204800: --connbytes-dir both --connbytes-mode bytes \
    -j TOS --set-tos Maximize-Throughput
$IPTABLES -t mangle -A OUTPUT \
    -m connbytes --connbytes 204800: --connbytes-dir both --connbytes-mode bytes \
    -j TOS --set-tos Maximize-Throughput

# отдельные правила для Jabber - максимальный приоритет
$IPTABLES -t mangle -A INPUT \
    -p tcp -m multiport --source-ports 5222:5223 \
    -j TOS --set-tos Minimize-Delay
$IPTABLES -t mangle -A OUTPUT \
    -p tcp -m multiport --destination-ports 5222:5223 \
    -j TOS --set-tos Minimize-Delay

Все это работает для "классических" соединений, но не работает для торрентов (и, наверняка, для других p2p протоколов), т.к. в этом случае открывается множество короткоживущих соединений, через каждое из которых передается лишь небольшое количество данных. Данная схема отнесет все эти соединения в новым и назначит им наивысший приоритет, что неприемлемо.

Можно поиграть с настройками торрент клиента. Ktorrent позволяет, во-первых, устанавливать поле TOS своих пакетов (точнее, поле DSCP, что, по сути, лишь более новый стандарт интерпретации того же байта IP пакета), а во-вторых, задавать ширину используемого программой канала (в том числе и менять ее по расписанию). Первая настройка, к сожалению, не поможет с входящим трафиком, тут надо, чтобы ваши "напарники" устанавливали тип трафика. Вторая настройка дает не совсем то, что нам нужно. Нужно, чтобы торрент использовал весь канал, и лишь ненадолго уступал его браузеру, а не то, чтобы торрент использовать лишь часть канала, а остальная часть всегда бы принадлежала браузеру.

Я применил довольно грубое, но более-менее работающее, решение. Любому трафику с непривилегированных портов (которые больше 1024) на непривилегированные порты я назначаю наименьший приоритет. В эту категорию не попадает обычный веб-трафик (на 80 или 443 порты), что нам и нужно. Правила надо вставить до специфических правил для отдельных портов (до Jabberа):

$IPTABLES -t mangle -A INPUT \
    -p tcp --sport 1024:65535 --dport 1024:65535 \
    -j TOS --set-tos Maximize-Throughput
$IPTABLES -t mangle -A OUTPUT \
    -p tcp --dport 1024:65535 --sport 1024:65535 \
    -j TOS --set-tos Maximize-Throughput

Предлагаю еще один, более хитрый и правильный, способ определения торрент трафика. Только в качестве экспериментального и непроверенного. Во-первых, необходимо определить пакеты, создаваемые определенным приложением - торрент-клиентом. Тут поможет модуль owner. К сожалению, определение PID и названия процесса не работает на многопроцессорных машинах и может быть отключено в вашем ядре. Во-вторых, надо отделить входящие пакеты для этого приложения. Тут работает модуль connmark. Вот что может получиться (не проверялось!):

$IPTABLES -t mangle -A OUTPUT \
    -m owner --cmd-owner ktorrent \
    -j CONNMARK --set-mark 1
$IPTABLES -t mangle -A INPUT \
    -m connmark --mark 1 \
    -j TOS --set-tos Maximize-Throughput
$IPTABLES -t mangle -A OUTPUT \
    -m connmark --mark 1 \
    -j TOS --set-tos Maximize-Throughput

Читайте man iptables.

Если захочется более серьезно поиграть с управлением трафиком, смотрите tcng. Это язык и компилятор для более дружелюбного, чем позволяет стандартная утилита tc, создания правил управления трафиком.

Наблюдение за трафиком

Для контроля собственного трафика достаточно наблюдать трафик на интерфейсе. Это не будет полноценным биллингом, нельзя будет выяснить, куда ушел трафик. Но сверить сумму с данными провайдера - можно.

Очень хорош collectd. Это легкий демон, который собирает множество параметров вашего компьютера. Включая и показания счетчиков интерфейсов. В новой версии (4.3.0) демон научился снимать показания счетчиков интерфейсов различных устройств по SNMP. Теперь MRTG не нужен :)

collectd хранит данные в файлах RRD. Этот формат специально предназначен для хранения числовых данных за периоды времени с автоматическим расчетом максимальных, минимальных и средних значений. Также по данным легко стоятся довольно сложные графики.

Настраиваем демон. Правим файл /etc/collectd/collectd.conf:

# Конфигурационный файл для collectd(1)
#
# Некоторые плугины требуют дополнительной конфигурации и поэтому
# по умолчанию отключены. Подробности смотрите в collectd.conf(5).
#
# Также следует прочитать /usr/share/doc/collectd/README.Debian.plugins
# прежде чем включать какой-либо плугин.

# интервал по умолчанию - 10 секунд
# я увеличил его до 30 секунд, т.к. мне не удалось установить интервал SNMP опроса
Interval 30

LoadPlugin battery
LoadPlugin cpu
LoadPlugin df
LoadPlugin disk
LoadPlugin entropy
LoadPlugin interface
LoadPlugin irq
LoadPlugin load
LoadPlugin memory
LoadPlugin processes
LoadPlugin rrdtool

# этот плугин был выключен
LoadPlugin snmp

LoadPlugin swap
LoadPlugin syslog
LoadPlugin tcpconns
LoadPlugin users

<Plugin rrdtool>
    DataDir "/var/lib/collectd/rrd"
</Plugin>

# См. примеры более сложной конфигурации
# в /usr/share/doc/collectd/examples/snmp-data.conf.gz
<Plugin snmp>
#   какие параметры будем получать по SNMP
    <Data "std_traffic">
        Type "if_octets"
        Table true
        Instance "IF-MIB::ifDescr"
        Values "IF-MIB::ifInOctets" "IF-MIB::ifOutOctets"
    </Data>
#   адрес хоста и параметры доступа
    <Host "dlink">
        Address "10.200.200.1"
        Version 1
        Community "public"
        Collect "std_traffic"
        #Inverval 120
    </Host>
</Plugin>

<Plugin syslog>
    LogLevel info
</Plugin>

Include "/etc/collectd/thresholds.conf"

Для некоторых плугинов необходимо установить дополнительные пакеты, указанные в зависимостях качестве рекомендуемых для collectd. Для плугина ping нужен пакет liboping0 (этот плугин в этой версии collectd у меня не заработал). Для snmp - libsnmp15.

Статистика по локальным интерфейсам помещается в файл вида: /var/lib/collectd/rrd/asus/interface/if_octets-eth1.rrd. Здесь asus - имя локального хоста; eth1 - название интерфейса. Статистика, полученная через SNMP помещается в файл вида: /var/lib/collectd/rrd/dlink/snmp/if_octets-Ethernet-WAN.rrd. Здесь dlink - имя хоста, указанное в конфигурации; Ethernet-WAN - имя (описание) интерфейса, полученное через SNMP.

Теперь необходимо как-то извлечь собранные данные. Самый простой способ - воспользоваться CGI скриптом /usr/share/doc/collectd/examples/collection.cgi.gz. Распакуйте его куда-нибудь в cgi-bin имеющегося под рукой веб-сервера, сделайте исполняемым, и все, вы можете получить красивые графики всех доступных параметров. Скрипт написан на Perl, вам понадобится сам Perl плюс еще небольшая кучка модулей...

Собственно для манипуляции с RRD файлами предназначена утилита rrdtool, с кучей параметров и страниц мануалов. Нас пока интересует рисование графиков. Для этого предназначена команда rrdtool graph, которая создает картинки с графиками. Но все же удобнее нарисовать веб-страницу и поместить на нее графики. Для этого есть rrdcgi. CGI скрипт получается примерно такой:

#!/usr/bin/rrdcgi
<html>
    <head><title>Интернет</title></head>
    <body>
    <h1>Загрузка интерфейса за сутки</h1>
    <p>
    <h2>eth1</h2>
    <RRD::GRAPH traffic-eth1.png
                --lazy
                --width 800
                -v "b/s"
                --title "eth1 traffic"
                DEF:incoming=/var/lib/collectd/rrd/asus/interface/if_octets-eth1.rrd:rx:AVERAGE
                DEF:outgoing=/var/lib/collectd/rrd/asus/interface/if_octets-eth1.rrd:tx:AVERAGE
                CDEF:in=incoming,8,*
                CDEF:out=outgoing,8,*
                AREA:in#00a000:"Incoming"
                LINE1:out#0000a0:"Outgoing">
    </p>
    <p>
    <h2>wan</h2>
    <RRD::GRAPH traffic-wan.png
                --lazy
                --width 800
                -v b/s
                --title "wan traffic"
                DEF:incoming=/var/lib/collectd/rrd/dlink/snmp/if_octets-Ethernet-WAN.rrd:rx:AVERAGE
                DEF:outgoing=/var/lib/collectd/rrd/dlink/snmp/if_octets-Ethernet-WAN.rrd:tx:AVERAGE
                CDEF:in=incoming,100000,GE,UNKN,incoming,8,*,IF
                CDEF:out=outgoing,100000,GE,UNKN,outgoing,8,*,IF
                AREA:in#00a000:"Incoming"
                LINE1:out#0000a0:"Outgoing">
    </p>
    <h1>Загрузка интерфейса за неделю</h1>
    <p>
    <h2>eth1</h2>
    <RRD::GRAPH traffic-eth1-week.png
                --lazy
                --width 800
                --start -1w --end now
                -v "b/s"
                --title "eth1 traffic"
                DEF:incoming=/var/lib/collectd/rrd/asus/interface/if_octets-eth1.rrd:rx:AVERAGE
                DEF:outgoing=/var/lib/collectd/rrd/asus/interface/if_octets-eth1.rrd:tx:AVERAGE
                CDEF:in=incoming,8,*
                CDEF:out=outgoing,8,*
                AREA:in#00a000:"Incoming"
                LINE1:out#0000a0:"Outgoing">
    </p>
    </body>
</html>

Кратко о параметрах (которые аналогичны таковым у rrdtool graph):

traffic-eth1-week.png
файл с графиком, который будет создан. Ссылка на этот файл будет вставлена в HTML.
--lazy
графики будут обновлены только если появились новые данные.
--width 800
ширина графика (без рамки) - 800 пикселей.
--start -1w --end now
начало графика - неделя тому назад (можно месяц и т.д., понимается "человеческий формат", т.е. -1year, -1month), окончание графика - сейчас.
--title "eth1 traffic"
заголовок графика, русские буквы, кажется, не воспринимаются.
DEF:incoming=/var/lib/collectd/rrd/asus/interface/if_octets-eth1.rrd:rx:AVERAGE
определяется "переменная" с именем incoming, данные берутся из указанного файла из источника данных под названием rx, берутся средние значения.
CDEF:in=incoming,8,*
определяется "производная переменная" с именем in, значениями которой являются значения incoming, умноженные на 8. Используется обратная польская запись. Умножать на 8 нужно, чтобы перевести байты (октеты) в секунду в биты в секунду.
CDEF:in=incoming,10000,GE,UNKN,incoming,8,*,IF
то же, что выше, но для SNMP. Аномальные очень большие значения, возникающие при перезагрузке устройства (счетчики обнуляются) заменяем на неопределенное значение.
AREA:in#00a000:"Incoming"
данные переменной in рисуем "заливкой" зеленым цветом и даем метку "Incoming".
LINE1:out#0000a0:"Outgoing"
данные переменной out рисуем тонкой линией синего цвета и даем метку "Outgoing".

Чтобы получить числовые значения трафика, нужно немного больше программирования. Я воспользовался Python, нужен модуль rrdtool из пакета python-rrd. Скрипт, который показывает входящий и исходящий трафик за день, выглядит так:

#!/usr/bin/python

import rrdtool

# файл статистики интерфейса
ETH1 = "/var/lib/collectd/rrd/asus/interface/if_octets-eth1.rrd"
# разрешение файла, здесь - 30 секунд
# значение должно равняться интервалу опроса collectd
RESOLUTION = 30

# выбираем отчеты
traffic = rrdtool.fetch(
        ETH1,
        "AVERAGE",
        "-r", str(RESOLUTION),
        "-s", "-1d")    # начиная день тому назад

incoming = 0
outgoing = 0

# cуммируем
for row in traffic[2]:
    if row[0]:
        incoming += row[0]
    if row[1]:
        outgoing += row[1]

# умножаем на интервал, переводим в мегабайты и выводим
print "Incoming:\t%g Mb" % (float(incoming)*RESOLUTION/1024/1024)
print "Outgoing:\t%g Mb" % (float(outgoing)*RESOLUTION/1024/1024)
К сожалению, этот простой способ неприемлем для подсчета трафика за большие периоды, т.к. тут разрешение, т.е. промежуток между отчетами будет переменным. Необходимо извлекать из RRD допольнительные данные. Есть, куда развивать метод...

Я совместил расчет трафика и генерацию графиков в одном CGI скрипте, написанном на Python. Это не сложно. В результате получается красивая страница со всеми нужными данными.

Читаем страницы мануалов:

См. также статью в журнале "Системный администратор".

Denis Nelubin, May 2008
Creative Commons License This work is licensed under a Creative Commons Attribution-Share Alike 3.0 License.