Новые мозги для старого друга
Есть в моем домашнем технопарке зверь пароды Buffalo из семейства сетевых накопителей - Buffalo Link station LS-QL. Купил я его по дешевке у вьетнамцев(!) на ebay. Потестировав накопитель пару дней, я понял почему хитрые азиаты от него избавились. Link station LS-QL оказался крайне тормозным NAS, его скорость чтения/записи данных по сети редко превышала 10 Мбит. Но даже такой черепашке нашлось применение благодаря встроенному BitTorrent-клиенту. Из сетевого накопителя получилась сносная торрент-качалка, почти бесшумная, с малым энергопотреблением и огромным дисковым пространством. Так у меня Buffalo и работал почти 24/7, выкачивая по ночам варез и сериалы, пока не начали происходить странные вещи. Сначала встроенный торрент-клиент отказался сидировать уже скаченное, а потом и вовсе перестал качать с некоторых трекеров. Я полез на официальный форум Buffalo где убедился что такая проблема появились не у меня одного. Ответ службы поддержки был кратким и циничным: «Новых прошивок не будет». После чего страдающие пользователи пошли разными дорогами. Кто-то решил что NAS пора отнести на помойку, другие же начали с переменным успехом устанавливать на него Transmission. Я выбрал срединный путь — купил новые мозги.
План
План заключался в следующем. Использовать NAS как сетевой диск, а торрент-качалку поставить на что-нибудь компактное, бесшумное… Например на Raspberry Pi? Например да, но я выбрал его более мощного корейского собрата Odroid U3.

Настраиваем мозги
Сначала определимся с операционной системой. С официального сайта можно скачать Android или Ubuntu. Мне хотелось чего-нибудь легковесного, минималистичного и без всяких иксов. Качаем отсюда сборку голого дебиана с ssh сервером. Качаем прогу для записывания образа на флешку. Новую версию программы можно поискать тут. Распаковываем сборку, запускаем программу, записываем на microSD, вставляем microSD в odroid… Включаем питание, моргает синий диод, подключаем сеть… пароль root, логин root… Здравствуй консоль! Редактируем /etc/apt /sources.list. Всё стираем и пишем что нам нужно:
deb http://ftp.ru.debian.org/debian/ jessie main
deb http://security.debian.org/ jessie/updates main
Обновляем кэш пакетов и сами пакеты:
aptitude update && aptitude safe-upgrade
Устанавливаем необходимые пакеты:
aptitude install rtorrent cifs-utils screen vsftpd python3
В качестве новой торрент-качалки выбрал консольный торрент-клиент rtorrent. Запускать rtorrent из-под root`а это неправильно и небезопасно, поэтому создадим пользователя torrentusr и зададим ему пароль:
useradd torrentusr -c "rtorrent user" –m
passwd torrentusr
Флаг -m указывает, что для пользователя создастся домашняя папка в /home (где папка создается по умолчанию можно посмотреть в выводе useradd –D) Теперь нам нужно подключить NAS как сетевую папку для этого мы установили cifs-utils. Создаем в /mnt папку torrents и назначаем ей владельца — torrentusr и группу владельца — torrentusr:
mkdir /mnt/torrents
chown torrentusr:torrentusr /mnt/torrents
Создаем на сетевом накопителе папку torrents_pit куда будут закачиваться торренты и которую мы будем монтировать к папке torrents. Попробуем смонтировать папку:
mount.cifs //192.168.1.14/torrents_pit /mnt/torrents -o guest,sec=none
Здесь мы папку torrents_pit , находящуюся на файловом ресурсе 192.168.1.14 монтируем к точке монтирования torrents. Фаиловую систему указываем как CIFS. После флага -о идут дополнительные опции:
guest - подсоединяемся без использования логина/пароля
sec=none - не используем протоколы сетевой аутентификации
И ещё. После флага -o все параметры должны идти через запятую, но без пробелов! Подмонтировался NAS? Если подмонтировался то в /etc/fstab добавляем строчку:
//192.168.1.14/torrent_pit /mnt/torrents cifs guest,uid=torrentusr,gid=torrentusr,iocharset=utf8,rw,file_mode=0644,dir_mode=0751,nounix, noexec,sec=none 0 0
Теперь torrent_pit будет монтироваться при загрузки debian. Разберем параметры:
cifs - используем в качестве сетевой файловой системы
guest - подсоединяемся без использования логина/пароля
sec=none - коннектимся без аутентефикации
nounix - важный для нас параметр. Он говорит команде mount что сервер с которого мы пытаемся подмонтировать папку не поддерживает CIFS Unix Extensions. Поэтому права доступа и владелец файлов назначаются на стороне клиента и клиент не пытается изменить эти значения на сервере. Назначаем:
uid=torrentusr - владельцем всех файлов и каталогов становится torrentusr
gid=torrentusr - для всех файлов и каталогов группа torrentusr
file_mode=0644 - все файлы монтируемой файловой системы получают указанные права доступа
dir_mode=0751 - все папки монтируемой файловой системы получают указанные права доступа
noexec - запрещает запуск на выполнение файлов с монтируемой файловой системы. Запрет действует даже если в правах файла выполнение будет разрешено
rw - файловая система монтируется в режиме чтения/записи, (ro - только чтение). Также как и noexec, правило read-only распространяется на все файлы и каталоги монтируемой файловой системы, даже если в их правах доступа есть разрешение на запись
Настройка rtorrent. Не легкая прогулка.
В планах у меня было рассказать какой rtorrent минималистичный и гибкий и как я за 5 минут написал для него конфиг. Но не тут то было… На то чтобы написать конфиг мечты и заставить rtorrent делать то что мне хочется, пришлось затратить несколько дней(!). Продравшись через дебри мутного синтаксиса, криво работающих функций и слабой документации, я всё-таки своего добился. Но легкой прогулки не получилось.
Базовая настройка
Чтобы добиться от rtorrent базового функционала много времени действительно не требуется. Логинимся как torrentusr и приступаем к настройке В сетевой папке torrents, создаем директории session и downloading, а в домашней дирректории torrentusr создаем файл .rtorrent.rc:
mkdir /mnt/torrents/session
mkdir /mnt/torrents/downloading
vi /home/torrentusr/.rtorrent.rc
Файл .rtorrent.rc содержит настройки rtorrent пишем в него:
#указываем диапазон, из которого rtorrent будет выбирать порт для работы
port_range = 6881-6881
#максимальная скорость скачивания, 0 - безлимит
download_rate = 0
#максимальная скорость отдачи, 0 - безлимит
upload_rate = 0
#здесь rtorrent сохраняет временные файлы и данные связанные с закачками
session = /mnt/torrents/session
#сюда закачиваются торренты
directory = /mnt/torrents/downloading
#пересчитываем хеш после закачки торрента
check_hash = yes
#включаем поддержку DHT сети
dht = auto
#DHT порт
dht_port = 8881
#включаем поддержку PEX для обмена списками пиров между клиентами
peer_exchange = yes
#директория в которой rtorrent ищет torrent файлы для закачки
schedule = start_directory, 5, 5,load.start=/mnt/torrents/newtorrents/*.torrent
Команда schedule здесь самая интересная. Команда выполняет указанную функцию с заданной периодичностью и имеет следующий формат:
schedule = name,start,interval,command
name - произвольное имя
start - через сколько секунд после своего запуска rtorrent выполнит command
interval - через сколько секунд command будет выполнена второй и все последующие разы
command - сама исполняемая функция
start и interval также могут задаваться в формате dd:hh:mm:ss.
Запустить rtorrent можно просто набрав в консоли:
rtorrent
Так как rtorrent это консольное приложение то его работа завершается вместе с закрытием SSH сеанса. Нас такое положение дел не устраивает, поэтому мы установили screen и запускать rtorrent будем с его помощью:
screen -dmS rtorrent rtorrent
После запуска rtorrent должен каждые 5 секунд проверять папку newtorrents на наличие новых торрент-файлов и ставить их на закачку. Тут же я столкнулся с первой проблемой… Но прежде чем начинать решать проблемы, надо ознакомиться с особенностями конфиг фаила rtorrent. Очень и очень рекомендую прочитать эту статью с хабры. Описание встроенных функций можно посмотреть здесь и еще вот здесь. Ну и для вдохновения статья хардкорного писателя конфигов для rtorrent.
Первая проблема
Если кинуть торрент-файл в папку newtorrents, то закачка успешно стартует. Но если эту закачку удалить и попробовать снова положить торрент-файл в newtorrents, то закачка повторно не запустится. А я вот люблю иногда что-нибудь посидировать из ранее скаченного. Проблема, как оказалось, появляется только когда вы записываете файл в папку из под Windows и связана с тем что rtorrent не хочет добавлять закачку с таким же торрент файлом как у ранее удаленной закачки. В контексте Linux это означает, что у файлов совпадают имена и поля индексного дескриптора. Когда я изменял имя торрент-файла или, например, время последнего доступа к нему, то закачка успешно стартовала. Время последнего доступа удобно изменять командой touch:
touch /mnt/torrent/newtorrent/some_file.torrent
Для выполнения bash-команд и скриптов у rtorrent есть команда execute. В домашней директории пользователя torrentusr создадим папку scripts, в ней скрипт touch.sh:
mkdir /home/torrentusr/scripts
vi /home/torrentusr/scripts/touch.sh
find /mnt/torrents/newtorrents/ -name "*.torrent" -exec touch {} \+
Скрипт ищет в папке newtorrent все файлы оканчивающиеся на .torrent и трогает их обновляя дату последнего доступа. Теперь добавляем новый schedule в конфиг rtorrent:
schedule = touch_this, 10, 10, "execute = sh, /home/torrentusr/scripts/touch.sh"
Проблема решена. Кстати, почему я создали отдельный скрип для всего одной команды find? А потому что параметры передаваемые в команду execute нужно правильно разделять запятыми и еще экранировать кавычки. Как тут правильно расставить запятые я так и не понял.
Дальше больше
Сейчас как уже закаченные, так и еще скачивающиеся торренты лежат в папке downloading. Это не очень удобно, сделаем так чтобы готовые торренты перемещались в отдельную папку. В rtorrent есть 12 ивентов по наступлению которых можно выполнять заданные функции:
'event.download.closed'
'event.download.erased'
'event.download.finished'
'event.download.hash_done'
'event.download.hash_queued'
'event.download.hash_removed'
'event.download.inserted'
'event.download.inserted_new'
'event.download.inserted_session'
'event.download.opened'
'event.download.paused'
'event.download.resumed'
Из названий в принципе ясна суть ивентов. Нам нужен event.download.finished который наступает когда торрент закачен на 100%. Создаем папку completed:
mkdir /mnt/torrents/completed
И добавляем в .rtorrent.rc строчку:
system.method.set_key = event.download.finished, move_file, "d.set_directory=/mnt/torrents/completed;execute=mv,-u,$d.get_base_path=,/mnt/torrents/complited;d.save_full_session="
move_file - произвольное имя, про его назначение я позже расскажу
d.set_directory - указываем rtorrent папку где будут находится файлы торрента
d.get_base_path - возвращает путь к файлам торрента
d.save_full_session - если вы не добавив эту функцию перезапустите rtorrent, то присвоенное d.set_directory значение будет потеряно и выскочит ошибка Inactive: Download registered as completed, but hash check returned unfinished chunks
Теперь не заходя в rtorrent мы будем знать, какие торренты уже скачались а какие еще качаются. Сделать так чтобы и удалять закачки из rtorrent можно было также не заходя в него:
schedule = remove_directory, 5, 5, remove_untied=
system.method.set_key = event.download.erased, 1_delete_erased, "execute=rm,-fr,$d.get_base_path="
В rtorrent есть понятие tied файлов. Когда вы добавляете торрент-закачку, rtorrent копирует указанный .torrent файл в папку session, переименовывает его в хеш_торрента.torrent и далее работает уже с ним. Но ассоциация со старым торрент-файлом остается и если его удалить, то закачка в rtorrent получает статус untied. Этим мы и воспользовались каждые 5 секунд запуская функцию remove_untied. Также добавили ивент event.download.erased, который сработает после запуска remove_untied и удалит закаченные торрентом файлы.
Хорошо, удалять закачки мы тоже научились, но появилась очередная проблема. По названию не всегда легко понять с какой конкретно закачкой связан данный .torrent файл, находящийся в папке newtorrents. Возник следующий план. После добавления торрент-файла в newtorrents, в completed создаем папку с именем новой закачки, копируем туда tied торрент-файл, а когда торрент будет скачан на 100% туда же перенесем файлы закачки. Voodoo код:
#создаем функцию которая выполняет конкатенацию строк и возвращает новый путь к папке с файлам торрента
system.method.insert = d.dir_for_torrent,simple,"cat=/mnt/torrents/complited/,$d.get_name="
#возвращает путь к .torrent файлу со случайным названием
system.method.insert = d.renamed_torrent_file_path,simple,"cat=$d.dir_for_torrent=,/,$system.time=,.torrent"
#создаем директорию с именем торрент-закачки
system.method.set_key = event.download.inserted_new, 1_create_dir, "execute=mkdir,-p,$d.dir_for_torrent="
#в переменной сохраняем путь к .torrent файлу
system.method.set_key = event.download.inserted_new, 2_save_old_path, "d.set_custom1=$d.get_loaded_file="
#копируем .torrent файл в созданную папку, даем файлу случайно сгенерированное имя, делаем его tied файлом
system.method.set_key = event.download.inserted_new, 3_set_new_torrent_file, "d.set_custom2=$d.renamed_torrent_file_path=;execute=cp,-n,$d.get_loaded_file=,$d.get_custom2=;d.set_tied_to_file=$d.get_custom2="
#переносим в ту же папку изначальный .torren файл, чтобы сохранить его название
system.method.set_key = event.download.inserted_new, 4_move_old_torrent_file, "execute=mv,-u,$d.get_custom1=,$d.dir_for_torrent="
#очищаем переменные и не забываем применить d.save_full_session
system.method.set_key = event.download.inserted_new, 5_clear_var_and_save_ses, "d.set_custom1=;d.set_custom2=;d.save_full_session="
Поясню несколько моментов, поскольку написание этого кода испортило мне весь день:
Если вы к одному ивенту цепляете несколько функций, то при наступлении ивента связанные с ним функции будут вызываться после лексикографической сортировки имен функций. Так что в данном случае имя функции имеет значение;
Функция d.set_tied_to_file не срабатывает если имя нового tied файла такое же как у предыдущего. Команда touch тут уже не помогает. Поэтому я стал генерить новое имя файла на основе функции system.time, которая возвращает текущее POSIX-время;
Ивент inserted_new возникает при добавлении торрент-файла. Ивент inserted_session - при добавлении торрент-файла из папки session в случае перезапуска rtorrent. Ивент inserted - это объединение inserted_session и inserted_new. То есть inserted = inserted_new + inserted_session;
Для хранения промежуточных значений используем предопределенные переменные custom1 и custom2. Вообще их 5 штук: custom1-5;
Опять же не забываем про d.save_full_session иначе в случае перезапуска rtorrent закачки будут удалены.
Переписываем ивент finished:
system.method.set_key = event.download.finished, move_finished, "d.set_directory=$d.dir_for_torrent=;execute=mv,-u,$d.get_base_path=,$d.dir_for_torrent=;d.save_full_session="
И добавляем еще один ивент erased, поскольку после удаления торента надо будет удалять его папку:
system.method.set_key = event.download.erased, 2_delete_erased, "execute=rm,-fr,$d.dir_for_torrent="
Давняя мечта
Мучения не прошли даром, теперь rtorrent+odroid ничем не уступают по удобству той торрент-качалке, что встроена в NAS. Осталось осуществить свою давнюю мечту и сделать так чтобы торренты не забивали мой интернет канал тогда когда он мне нужен. Решить вопрос можно с помощью настройки QoS или установкой гибких ограничений на скорость закачки. Первый вариант отпадает, если ваш провайдер не поддерживает QoS. А вот реализация второго зависит от возможностей торрент-клиента. Про планировщики(schedule) в rtorrent я уже писал. Можно настроить планировщики так чтобы rtorrent ограничивал скорость закачки в нужные часы и качал на полную по ночам. Но этот путь недостаточно джедайский, ведь засидевшись до ночи придется лезть в терминал и остановливать закачки. Сделаем последнее усилие по настройке rtorrent чтобы получить в итоге торрент-клиент мечты.
Самое гибкое правило по ограничению скорости которое я смог придумать заключается в следующем: пусть на одройде работает какой-нибудь скрипт, который с определенными интервалами времени пингует все устройства в моей домашней сети. Если хотя бы одно устройство обнаружено, то скрипт должен заставить rtorrent остановить закачки. Когда устройсво уйдет в оффлайн, все закачки должны быть возобновлены на максимальной скорости. Возьмем в руки Python и напишем такой скрипт:
#!/usr/bin/python
import subprocess
from multiprocessing.pool import ThreadPool
pool = ThreadPool(54)
home_network = '192.168.1.'
intf = 'eth0'
odroid_ip = subprocess.Popen('ip address show dev ' + intf, shell=True,
stdout=subprocess.PIPE).communicate()[0]
odroid_ip = (odroid_ip.decode("utf-8")).split()
odroid_ip = odroid_ip[odroid_ip.index('inet') + 1].split('/')[0]
odroid_last_octet = int(odroid_ip.split('.')[3])
hosts = map(lambda octet: home_network + str(octet),
[i for i in range(200, 254) if i != odroid_last_octet])
result = pool.map(lambda ip: subprocess.call('ping -c 1 ' + ip, shell=True,
stdout=open('/dev/null', 'w'),
stderr=subprocess.STDOUT), hosts)
pool.close()
pool.join()
for i in result:
if i == 0:
print('d.pause=')
break
else:
print('d.resume=')
Код питона прекрасен даже в моих корявых руках. Обратите внимание на библиотеку multiprocessing.pool, которая позволяет сделать и без того полезную функцию map еще и многопоточной. Число потоков задается в ThreadPool(). Вызванная без параметров, функция создаст число потоков равное числу ядер процессора. Подбирать оптимальное число потоков надо опытным путем. Замеряя с помощью time время работы скрипта, я заметил, что в данном случае оптимально, когда число потоков равно числу пингуемых ip:
time python3 /home/torrentusr/scripts/pinger.py
real 0m3.478s
user 0m0.255s
sys 0m0.575s
Всего 3 секунды! Очень неплохо. Донастроим rtorrent:
system.method.insert = ping_network,simple,"execute.capture=python3, /home/torrentusr/scripts/pinger.py"
Новая функция использует execute.capture. Помимо выполнения скрипта execute.capture вернет его вывод.
schedule = someone_online, 30, 300, "d.multicall=main,$ping_network="
Функция d.multicall применяет заданную последовательность команд ко всем торрентам данной… эээ… таблицы(view). Приведу картинку чтобы было понятно что имеется ввиду:

Таблицы вызываются клавишами 1-0 и показывают списки активных, остановленных, качающихся и раздающихся торрентов. В данном случае в d.multicall указана таблица main то есть таблица содержащая все закачки. Ко всем торрентам будет применен вывод функции ping_network (d.pause или d.resume).
Превращаем Odroid в seedbox
На работе мне приходится пользоваться yota брелком. Как всем известно, Yota режет торрент трафик. Поэтому я решил превратить odroid в своеобразный seedbox, чтобы обойти йотовские ограничения. Для этого понадобится vsftpd, openssl и совсем немного времени. Также я использовал DDNS сервис noip.com чтобы из интернета всегда можно было достучаться до своего одройда. Поддержка noip.com уже была встроена в мой домашний роутер. Мы уже установили vsftpd поэтому открываем конфиг /etc/vsftpd.conf и пишем в него:
#работаем в standalone mode без помощи inetd/xinetd
listen=YES
#запрещаем анонимных пользователей
anonymous_enable=NO
#разрешаем вход для локальных пользователей
local_enable=YES
#разрешаем изменение фаиловой системы: удаление фаилов, создание папок и тд.
write_enable=YES
#локальные пользователи после входа будут “заточаться” в их домашнем каталоге после
chroot_local_user=YES
#вместо домашнего каталога пользователи будут “заточаться” в этой папке
local_root=/home/torrentusr/torrents
#под этим пользователем сервер работает, когда ему не нужны никакие привилегии
nopriv_user=ftpsecure
#включаем чтобы использовать имя хоста вместо ip адреса
pasv_addr_resolve=YES
#ip адрес для ответа на запрос PASV, при включенном pasv_addr_resolve резолвится имя хоста
pasv_address=torrents.no-ip.org
#диапазон портов для работы сервера в пассивном режиме
pasv_max_port=31254
pasv_min_port=31254
#пустой каталог который используется когда vsftpd не нужен доступ к файловой системе
secure_chroot_dir=/var/run/vsftpd/empty
#поддержка безопасных соединений с помощью SSL
ssl_enable=YES
#задает расположения RSA сертификата для использования в SSL и TLS зашифрованных соединениях
rsa_cert_file=/etc/ssl/localcerts/vsftpd.pem
Конфиг получился очень небольшим, потому что я не стал в нем прописывать те параметры дефолтные значения которых меня устраивали. С параметрами можно ознакомиться тут и тут. До окончательного успеха осталось всего пара шагов. Создадим системного пользователя ftpsecure:
useradd –r ftpsecure
Мы запретили вход на ftp сервер анонимусам и разрешили локальным пользователям. Кроме того, после присоединения к серверу все локальные пользователи будут отправлены в папку torrents и за пределы этой папки и её поддиректорий уйти не смогут. Наличие у пользователя прав записи в корень chroot папки vsftpd считает небезопасным и будет выдавать ошибку “refusing to run with writable root inside chroot ()“ при соединении с сервером. Проблема решается двумя способами. Можно отобрать у пользователя права на запись в папку:
chmod a-w torrents
Или просто прописать в конфиге:
allow_writeable_chroot=YES
Все данные, включая имена пользователей и пароли, ftp протокол передает в открытом виде. В vsftpd.conf мы уже пописали использование SSL шифрования, осталось сгенерировать сертификат:
mkdir /etc/ssl/localcerts
openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout /etc/ssl/localcerts/vsftpd.pem -out /etc/ssl/localcerts/vsftpd.pem
req - создать сертификат
-x509 - создавать самоподписанный сертификат
-days - сколько дней сертификат действует, по умолчанию 30
-nodes - созданный приватный ключ не будет зашифрован паролем
-newkey rsa:1024 - создаем новый приватный ключ RSA 1024 бит
-keyout - файл в который записывается новый приватный ключ (у нас туда же куда и сертификат)
-out - файл куда пишем сертификат
Перезапускаем FTP сервер и радуемся жизни:
service vsftpd stop
service vsftpd start
Готовые скрипты качаем тут.
Литература
rtorrent:
- Разбираемся с rtorrent всерьёз
- Базовая настройка rtorrent
- Кошерная настройка rtorrent и пояснения к конфигу .rtorrent.rc
- Готовые рецепты для rtorrent
- Описание функций и методов rtorrent
- Еще описание
- Описание функций и полей конфига rtorrent
- Starting rTorrent Behind Screen