TopolA MS В начало


Простой коллектор трафика на базе iptables (Linux) IPTStat
2005 © Oleg Vlasenko (vop@unity.net)
Обращайте внимание на год написания статьи!!!

1. Небольшой обзор

Одной из важных задач системы, при помощи которой предоставляются услуги пользователям кабельных сетей, является компонента, называемая коллектором трафика - т.е. механизм, позволяющий подсчитывать количество услуг (объем трафика), предоставленных пользователям. Существует огромное множество различных решений данной задачи при помощи различных методов, начиная от дорогих аппаратных комплексов и дорогих коммерческих программных продуктов, и заканчивая разного рода простыми скриптами. Все из них имеют свои достоинства и свои недостатки. Но в любом случае, администратор системы выбирает одно из них, исходя из своих критериев оценки стоимости, надежности и точности работы.

Предлагаемый ниже трафик коллектор можно назвать еще одним решением. При этом попробуем разобраться во всех недостатках и достоинствах предлагаемого решения, и сделать общую оценку его применимости в небольших коммерческих кабельных сетях (до 200-300 пользователей на роутер). Основная цель данного повествования - продемонстрировать простоту создания совместимого с TopolA решения. Помимо этого данный продукт поможет администраторам самим создавать простые системы сбора статистики при использовании отдельных частей данного продукта.

1.1. iptables

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

Для начала давайте посмотрим достоинства сетевого фильтра iptables с точки зрения подсчета трафика. В общем случае, iptables совершенно замечательная вещь. Во первых, любое правило фильтра имеет свой собственный счетчик, подсчитывающий количество пакетов и байт, попадающих под правило. Во вторых, забавная система хуков (hooks), при помощи которой обрабатывается трафик, проходящий через сервер/роутер, позволяет счетчикам работать в рамках обычного процесса роутинга. Фактически, проходящие через процесс роутинга пакеты подсчитывают сами себя. Если добавить, что весь процесс происходит на уровне ядра без передачи трафика в user-space пространство, и не требует никаких дополнительных программ, кроме собственно TCP/IP стека ядра (который и так есть, ибо без него сеть работать не будет), мы получим совершенно надежное средство для подсчета статистики, которое практически не нагружает систему, и в котором будут отсутствовать какие-либо "потери" даже при сильной нагрузке на роутер/сервер. Так-что использование iptables представляется мне наиболее идеальным средством для учета трафика на роутерах под управлением Linux, если бы не парочка небольших НО!.

И так, какие же подводные камни могут подстерегать администратора при использовании iptables в качестве счетчика трафика? Доступ к информации iptables, обеспечивает системная библиотека libipt, с помощью которой собрана и сама команда iptables. Библиотека работает по следующему принципу - для чтения таблиц создается их копия в user-space пространстве при помощи системных средств. Само по себе это не создает проблемы. Проблема начинается тогда, когда возникает необходимость внести какие-либо изменения, добавить или убрать правило. Дело в том, что любые одиночные изменения производятся в прочитанной копии таблиц, и после того, как эти изменения сделаны, вся копия таблиц записывается обратно в системную область ядра, перезаписывая в том числе и те показания счетчиков, которые были прочитаны вначале. А ведь за этот промежуток времени через ядро может пройти несколько сетевых пакетов, которые увеличат системные счетчики, но значения которых будет "затерто" предыдущим значением. Хоть потери в данном случае незначительны, но все же эта мелочь может отравлять администратору жизнь, не позволяя ему спать спокойно. Ниже мы попробуем минимизировать негативные последствия этого недостатка.

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

1.2. Выбор таблицы

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

В настоящий момент iptables поддерживает три встроенные таблицы - filter, nat и mangle. Таблица nat не совсем подходит для наших целей в силу специфичных встроенных цепочек PREROUTING и POSTROUTING. Еще одна таблица - filter в принципе хорошо подходит для подсчета трафика. Но, как правило, данная таблица обычно активно используется для динамического управления фаерволом, доступом клиентов к сети при помощи систем авторизации, и тому подобное. А вот таблица mangle в большинстве случаев либо вообще не используется, либо содержит фиксированный, один раз настроенный, перечень правил. Если это так, что выбор таблицы mangle представляется для нашего случая весьма удачным. Данная таблица содержит необходимые для подсчета трафика цепочки - INPUT, OUTPUT и FORWARD. Последняя из них подходит для подсчета транзитного трафика.

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

1.3. Выбор метода чтения счетчиков

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

Для считывания счетчиков iptables можно воспользоваться несколькими разными способами. Самый популярный среди сисадминов способ, который я встречал, заключается в том, что для чтения счетчиков последовательно вызывается утилита "iptables -L -n ..." для каждой цепочки счетчиков. При этом вывод цепочки либо записывается в файл для дальнейшей обработки, либо через pipe отдается на вход программе, обрабатывающей показания счетчиков.

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

Пропущу способ чтения счетчиков программой, написанной с использованием libipt в виду его очевидности. Существует еще один вариант считываения счетчиков iptables. Многие сисадмины почему-то не часто обращают внимание на существование другой утилиты для чтения таблиц iptables, а именно iptables-save. По сути, это и есть программа, написанная с использованием libipt и предназначена для простого считывания и вывода всего набора цепочек iptables "одним скопом". Надо заметить, что данная утилита работает существенно быстрее, чем базовая утилита "iptables", создавая минимальную нагрузку на вычислительные ресурсы роутера. Результат вывода этой утилиты содержит абсолютно все необходимые нам данные, включая все цепочки и счетчики. Именно эту информацию можно в дальнейшем обрабатывать, накапливая значения счетчиков в базе. Так-что мне представляется использование именно этой утилиты наиболее оптимальным для построения простого трафик-коллектора.

2. Строим цепочки коллектора трафика

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

2.1. Цепочки счетчиков клиентов

Для начала необходимо определиться с вопросом - а что же именно мы будем считать нашим клиентам? Чаще всего в домашних сетях ведется подсчет двух типов трафика - городской интернет и внешний (мировой) интернет. При этом каждый тип трафика учитывается по двум направлениям - входящий и исходящий. Т.е. получается всего 4 категории трафика. По большому счету, совершенно не важно, сколько именно категорий необходимо считать, но для простоты будем рассматривать именно вариант с 4-мя категориями.

И так, для подсчета трафика 4-х категорий нам понадобиться создать 4 отдельные цепочки. Дадим названия цепочкам таким образом, что бы было понятно их назначение. Например:

Каждая из этих цепочек будет содержать список всех IP клиентов с одним единственным правилом: -j RETURN. Исключительное назначение строк этих цепочек - "держать" счетчики для каждого клиента. Таким образом, для каждого клиента будет создано 4 строки (счетчика) - по одному в каждой из 4-х цепочек. В дальнейшем, выбирая трафик по определенным условиям, и направляя его в одну из 4-х цепочек, мы просто будем подсчитывать трафик для каждого клиента.

Для пример, если у нас есть три клиента с IP адресами 10.22.1.15, 10.22.1.16 и 10.22.1.17, то эти цепочки будут выглядеть следующим образом:

inp_city и inp_world одинаковые:

-d 10.22.1.15 -j RETURN
-d 10.22.1.16 -j RETURN
-d 10.22.1.17 -j RETURN


out_city и out_world одинаковые:

-s 10.22.1.15 -j RETURN
-s 10.22.1.16 -j RETURN
-s 10.22.1.17 -j RETURN


Как видно из примера, строки цепочки достаточно простые, без каких либо условий, кроме адреса получателя (для первой таблицы) или адреса отправителя (для второй).

Теперь для полноты картины стоит привести пример стартового скрипта для создания таких цепочек. Для начала нам необходимо создать необходимые цепочки. Если же цепочка уже существует, то неплохо бы ее очистить - мало ли, что в ней содержится. Для этого можно воспользоваться таким кодом в стартовом скрипте:

#!/bin/sh
[....]

# Check if Chain N1 exists
iptables -t mangle -F inp_city > /dev/null 2>&1
if [ x$? != x0 ]; then
  # Create Chain N1 (city input)
  iptables -t mangle -N inp_city
fi

# Check if Chain N2 exists
iptables -t mangle -F out_city > /dev/null 2>&1
if [ x$? != x0 ]; then
  # Create Chain N2 (city output)
  iptables -t mangle -N out_city
fi

# Check if Chain N3 exists
iptables -t mangle -F inp_world > /dev/null 2>&1
if [ x$? != x0 ]; then
  # Create Chain N3 (world input)
  iptables -t mangle -N inp_world
fi

# Check if Chain N4 exists
iptables -t mangle -F out_world > /dev/null 2>&1
if [ x$? != x0 ]; then
  # Create Chain N4 (world output)
  iptables -t mangle -N out_world
fi

[....]

Теперь у нас есть необходимые цепочки для подсчета 4-х категорий трафика, но они пока пустые. Нам необходимо еще заполнить их строками для всех наших клиентов. Предположим, список всех IP адресов наших клиентов находятся в файле - по одному адресу в каждой строке. Пусть наш файл будет /usr/local/accnt/conf/internet.lst Тогда наполнить наши цепочки можно примерно следующим фрагментом стартового скрипта:

#!/bin/sh
[....]

# Add counters from clients list
cat /usr/local/accnt/conf/internet.lst | while read Res; do
  iptables -t mangle -A inp_city -d $Res -j RETURN
  iptables -t mangle -A out_city -s $Res -j RETURN
  iptables -t mangle -A inp_world -d $Res -j RETURN
  iptables -t mangle -A out_world -s $Res -j RETURN
done

[....]

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

2.2. Цепочки селектора трафика

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

Для простоты дадим названия этим цепочкам trafinp и trafout. Во первых, нам необходимо создать эти цепочки, или очистить. При создании цепочек надо сделать дополнительно еще одну вещь - подключить две наши цепочки селекторов к главной цепочке сетевого фильтра FORWARD. Для определения направления трафика можно использовать правило, в котором учесть внешний интерфейс роутера. Предположим внешний канал нашего роутера подключен к интерфейсу eth1. Если цепочки уже существуют и мы их просто очищаем, то добавлять их вызов из цепочки FORWARD не следует - логично предположить, что они там уже добавлены. Тогда фрагмент скрипта, подключающего селектор будет выглядеть следующим образом:

#!/bin/sh
[....]

# Check if Chain Input exists
iptables -t mangle -F trafinp > /dev/null 2>&1
if [ x$? != x0 ]; then
  # Create Chain Input
  iptables -t mangle -N trafinp
  iptables -t mangle -I FORWARD -i eth1 -j trafinp
fi

# Check if Chain Output exists
iptables -t mangle -F trafout > /dev/null 2>&1
if [ x$? != x0 ]; then
  # Create Chain Output
  iptables -t mangle -N trafout
  iptables -t mangle -I FORWARD -o eth1 -j trafout
fi

[....]

Теперь необходимо заполнить цепочки селектора строками с информацией. Предположим список IP сетей, принадлежащих городским ресурсам находятся в файле /usr/local/accnt/conf/cityip.lst. Нам надо заполнить цепочки таким образом, что бы весь трафик проходящий с или на любой адрес из этого списка направлялся в цепочки city_*, а остальной трафик - в цепочки *_world. Это можно сделать при помощи следующего фрагмента скрипта:

#!/bin/sh
[....]

# Add city selectors
cat /usr/local/accnt/conf/cityip.lst | while read Res; do
  iptables -t mangle -A trafout -d $Res -s 0/0 -j out_city
  iptables -t mangle -A trafout -d $Res -s 0/0 -j RETURN
  iptables -t mangle -A trafinp -d 0/0 -s $Res -j inp_city
  iptables -t mangle -A trafinp -d 0/0 -s $Res -j RETURN
done

# And add world chains to the end of selector (rest)
iptables -t mangle -A trafout -j out_world
iptables -t mangle -A trafout -j RETURN
iptables -t mangle -A trafinp -j inp_world
iptables -t mangle -A trafinp -j RETURN

[....]

Хочу обратить внимание на две небольшие особенности данного фрагмента. Первая - после циклического добавления в селектор всех ресурсов, относящихся к городским, следуют команды добавления цепочек внешних ресурсов. И вторая особенность, о которой часто забывают системные администраторы - для каждого правила селектора создается по две строки с одинаковыми условиями, но с разными целями. Сначала идет строка, отдающая трафик в одну из цепочек со счетчиком, а следом за ней идет строка с целью RETURN. Это очень важная деталь, так-как после обработки пакета данных цепочкой счетчика, пакет будет возвращен в селектор. Что бы предотвратить повторный подсчет того же самого пакета данных, например, последним правилом, и не прибавлять городской трафик к внешнему, и необходимо второе правило, которое завершит обработку селектором и вернет пакет на уровень выше.

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

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

Пример скрипта, создающего необходимые цепочки, находится в комплекте программы iptstat, которая описана ниже, и называется rc.makestat. Вызов данного скрипта производится вторым скриптом get_stat.sh в случае такой необходмости. Скрипт написан таким образом, что может запускаться повторно, корректно пересоздавая структуру счетчиков.

3. Сохранение данных счетчиков

После того, как все цепочки коллектора трафика создаются и корректно считают трафик, возникает следующая задача - регулярно "снимать" показания этих счетчиков и записывать в какую-либо базу.

Для решения этой задачи предлагается небольшая программка, написанная на С. Программка называется iptstat. Принцип ее работы очень прост. Периодически, например, один раз в минуту, вызывается системная утилита iptables-save, и вывод этой утилиты подается на вход программы iptstat.

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

Далее рассмотрим программу iptstat подробнее.

3.1. Парсер счетчиков

Файлы iptstat.c (iptstat.h) содержат код основного тела программы и парсера таблиц iptables. Перед компиляцией программы необходимо сделать некоторые настройки для нашей системы. Для упрощения программы, конфигурация сделана прямо в теле программы. Тут стоит уделить внимание только трем переменным.

Первая переменная char *acc_stat_dir. В эту переменную заносится путь к каталогу, в котором будет сохраняться база с накопленными показаниями счетчиков. По умолчанию там записан "стандартный" путь - /usr/local/accnt/stat/.

Вторая переменная - char *ipt_save_file. Если эта переменная равна NULL, то программа предполагает, что информация поступает из входного потока. Как альтернатива, можно указать в этой переменной путь и имя файла. В этом случае программа будет полагать, что в этот файл записан вывод системной утилиты iptables-save. Иногда это бывает удобно, если выводом данной утилиты пользуется несколько разных программ, или в целях отладки.

И еще одна переменная, или точнее, массив переменных struct ipt_names_s ipt_names[]. Данный массив содержит описание цепочек со счетчиками, которые необходимо "выбирать" из общего потока данных iptables-save. Другими словами, в этой таблице описаны названия цепочек и каждой категории дан порядковый номер (начиная с 0), равный позиции счетчика в базе TopolA. Последний элемент в данном массиве должен содержать значение NULL вместо названия таблицы.

Рассмотрим вариант конфигурирования данного массива для нашего примера с 4-мя категориями трафика. В этом случае инициализация переменной должна выглядеть следующим образом:

iptstat.c:
[....]

struct ipt_names_s ipt_names[] = {
  {"inp_city", "-d", NULL, 0},
  {"out_city", "-s", NULL, 1},
  {"inp_world", "-d", NULL, 2},
  {"out_world", "-s", NULL, 3},
  {NULL, NULL, NULL, 0}
};

[....]

Как видно, счетчики inp_city получают порядковый номер 0, out_city - 1, и так далее. В дальнейшем везде, где мы будем встречать 4 цифры с данными трафика (например, при создании тарифов, или при просмотре базы данных), мы должны придерживаться именно этого порядка следования цифр.

Как видно из примера, элементом данного массива является структура. Первое поле структуры - название цепочек с нашими счетчиками. Четвертое поле - порядковый номер категории счетчика. Третье поле структуры предназначено для "агрегации" счетчиков, и в обычных условиях его значение равно NULL.

Второе поле, которое может содержать "-d" или "-s" (или "-j"), предназначено для указания "направления" трафика. Или другими словами, обозначает, в качестве чего должен быть указан IP адрес клиента - в качестве отправителя (-s) или получателя (-d) для корректного подсчета трафика. Для нашего случая существует однозначное соответствие между названием цепочки и опцией. Например, цепочка inp_world может однозначно содержать только опцию "-d", так-как входящий трафик может иметь в качестве получателя пакетов только IP адрес клиента. Смысл данной опции возникает в том случае, если счетчики входящего и исходящего трафика объединены в одной цепочке. Например, вместо inp_city и out_city создается только одна цепочка city, в которой для каждого клиента записано по два правила для двух направлений трафика. В этом случае, данная опция должна соответствовать направлению трафика так, как это определяется в командной строке команды iptables при создании цепочки. Хотя, такое объединение для небольшого количества клиентов несколько упрощает структуру таблиц, но при количестве нескольких сотен строк объединять цепочки в одну не совсем разумно, так-как это несколько увеличивает время обработки транзитных пакетов.

Дополнительно программа iptstat проверяет наличие флагов "некорректного времени", выставляемых плагином системы TopolA, что бы исключить неверный подсчет трафика. Немного отвлекаясь от темы, надо заметить, что все сервера и роутеры, работающие в коммерческой системе, необходимо строго синхронизировать по времени, дабы избежать разного рода конфликтов с клиентами, которые платят за услуги деньги. Система TopolA устроена таким образом, что в случае расхождения времени на серверах и роутерах в несколько секунд, система приостанавливает подсчет количество предоставленных услуг до устранения проблемы.

3.2. Накопитель данных

Вторая часть программы выполняет достаточно простую функцию. Она получает от парсера три переменные - IP адрес клиента, показание счетчика и порядковый номер категории, к которой это показание относится.

Накопитель написан в виде отдельного файла с исходным кодом st_count.c (в комплекте st_count.h). Данный модуль совершенно независим и может быть использован в качестве составной части при написании любой другой программы или утилиты, собирающей числовую информацию о количестве предоставленных услуг. Например, водопроводный электронный счетчик при использовании системы TopolA для управления большими домовладениями :).

Данный модуль имеет несколько переменных и функций. Конфигурирование модуля осуществляется присвоением переменным определенных значений. Первая переменная char *acc_stat_dir уже была описана выше, хотя она определена именно в этой части программы. Она устанавливает пусть к каталогу, в котором хранится накопительная база системы TopolA, в которую и заносятся данные. Данная переменная инициализируется в первой части программы iptstat, где она и была описана. В случае, если данная переменная не будет инициализирована, то она будет автоматически установлена в значение по умолчанию - /usr/local/accnt/stat/

Еще одна переменная int is_count_zer. По умолчанию ее значение равно 0. Это означает, что все значения трафика, передаваемые в модуль, являются "нарастающими", т.е. отражают состояние несбрасываемого возрастающего счетчика. Если данную переменную установить равной 1, то модуль будет расценивать поступающие данные, как значения сбрасываемого счетчика, которые должны просто добавляться к числу, хранящемуся в базе. В нашем примере мы используем несбрасываемые счетчики, поэтому значение этой переменной равно 0, а значение трафика вычисляется, как разница между предыдущим значением и текущим.

3.3. Скрипт запуска коллектора трафика

И последняя часть нашей системы - небольшой скрип для регулярного запуска коллектора трафика, например, из планировщика crond. Скрипт можно вызывать раз в минуту, что обеспечит достаточно регулярное обновление данных о количестве предоставленных услуг. Скрипт может быть очень простым. Например:

get_stat.sh:
#!/bin/sh

iptables-save -t mangle -c | /usr/local/bin/iptstat


Теперь достаточно добавить этот скрипт в crontab с вызовом раз в минуту, и можно считать, что мы выполнили нашу задачу. Надо отметить одну небольшую особенность. Дело в том, что утилиту iptables-save можно запускать только с полномочиями root. А система статистики обычно устанавливается с полномочиями специально созданного пользователя. Для системы TopolA обычно это пользователь "accnt". В этом случае после установки программ необходимо добавить атрибут +s бинарнику iptstat.

4. Утилита iptstat

Скачать архив с утилитой можно тут:

iptstat-0.2.tar.gz

Внимание! Текущую версию коллектора трафика (v0.3) можно скачать на странице поддержки клиентов. Получение исходников утилиты на странице поддержки киентов не требует никакой оплаты.

P.S. Приношу свои извинения, если я допустил какие-либо ошибки. В любом случае, можете связаться со мной по e-mail vop@unity.net.



Copyright © 2003-2017, Oleg Vlasenko