В предыдущей статье была рассмотрена установка и настройка tor в ОС Linux Mint 21.3, а также были рассмотрены несколько способов получения мостов obfs4 с помощью сайта и телеграмм бота. В комментариях был задан вопрос по поводу автоматического получения и обновления данных мостов. Если использовать рассмотренные ранее методы, скорее всего, получить мосты в автоматическом режиме не получиться. Но существует еще один, довольно интересный и не особо часто используемый метод, который рассмотрим в данной статье немного подробнее.
Еще одним, альтернативным способом получения мостов obfs4 является отправка письма на почту bridges@torproject.org. Вот, что сказано об этом на сайте:
Единственное ограничение в данном методе — это получение мостов не чаще одного раза в 3 часа. То есть, если вы сделали запрос мостов и благополучно их получили, то следующий запрос можно сделать только через три часа. Все последующие письма до истечения данного интервала времени будут просто игнорироваться. Да и закидывать сервер лишними запросами, скорее всего, не стоит. Он может решить, что письма с вашего ящика — спам и получить мосты данным способом больше не получиться.
Первый сервис электронной почты у нас недоступен. И туда можно попасть только окольными путями. Но, существует еще GMail, который, пока что, благополучно у нас функционирует. Поэтому, доступна некая альтернатива — без альтернативы, которую и будем использовать.
Дальше пойдет кривой код на питоне.
Получение пароля приложения
Так как мы будем использовать python, отправлять и получать с помощью скрипта почту, необходимо получить пароль приложения. Как это сделать достаточно подробно описано в справке Google, поэтому, останавливаться на данном шаге в статье, думаю не требуется. Когда будет получен пароль приложения, можно двигаться дальше.
Установка внешних библиотек
Для работы скрипта потребуется установка сторонних библиотек. Их можно установить с помощью команды:
pip install python-decouple opencv-python pyzbar IMAPClient
Так как мы будем использовать пароль приложений, а также логин от аккаунта Google, то в коде его желательно не использовать. Поэтому, необходимо создать файл «.env» с примерным содержимым:
login = "test@gmail.com"
pwd = "gffg sbbp jccj vhhi"
Для того, чтобы прочитать данные из этого файла нам потребуется библиотека decouple. Для чтения изображения с QR-кодом, а мы будем его использовать, так как это удобнее, чем парсить тело письма, потребуется библиотека opencv. Ну и для того, чтобы прочитать данные закодированные в QR-коде, используем библиотеку pyzbar. Вместо клиента imap идущего с Python из-коробки будем использовать стороннюю библиотеку IMAPClient, с помощью которой удобнее работать с сообщениями на почтовом сервере.
Импорт библиотек
После того, как библиотеки будут установлены, импортируем их и другие библиотеки, необходимые для работы, в скрипт:
import email
import platform
import re
import shutil
import smtplib
from socket import gaierror
import subprocess
import time
from datetime import datetime
from os import getuid
import cv2
from decouple import config
from imapclient import IMAPClient
from pyzbar.pyzbar import decode
Считывание данных из QR-кода
Создадим функцию qr_read(path), которая получает путь к изображению с кодом. Открывать и преобразовывать его в ndarray будем с помощью cv2, после чего сразу же передаем его для декодирования в функцию decode библиотеки pyzbar.
def qr_read(path):
try:
if img := decode(cv2.imread(path)):
briges = re.sub("['\[\]]", "", img[0][0].decode('utf-8')).split(", ")
return briges if briges else False
return False
except TypeError:
return False
Обработаем исключение, в случае, если не сможем прочитать изображение и вернем из функции False. Также преобразуем полученную строку с мостами, которая выглядит как список, но на самом деле им не является, в список, удалив из нее лишние символы и разбив по запятой с пробелом, после чего вернем его из функции.
Отправка письма
Напишем функцию mail_send(), которая будет отправлять письмо на указанный адрес с текстом для получения мостов. Здесь мы будем использовать стандартную библиотеку smtplib с SSL-шифрованием. Берем пароль и логин из файла «.env», и с помощью config(«login»), config(«pwd») подставляем в функцию авторизации. В случае с Gmail логин и адрес почты будут одинаковые (в моем случае). Авторизуемся и отправляем содержимое message. После чего закрываем соединение, выводим сообщение о том, что письмо отправлено и возвращаем из функции время отправки. Оно понадобиться в дальнейшем.
def mail_send():
try:
s = smtplib.SMTP_SSL("smtp.gmail.com", 465)
s.login(config("login"), config("pwd"))
message = "\r\n".join([
f"From:{config('login')}",
f"To: bridges@torproject.org",
f"Subject: ",
"",
'"get transport obfs4"'])
s.sendmail(config('login'), "bridges@torproject.org", message)
s.quit()
return datetime.utcnow().time()
except gaierror:
exit('Ошибка соединения')
except smtplib. SMTPAuthenticationError:
exit("Ошибка аутентификации. Проверьте логин или пароль")
Чтение почты
Для того, чтобы получить нужное нам письмо и забрать из него мосты, необходимо получить содержимое почтового ящика. В этом нам поможет библиотека IMAPClient.
Указываем адрес imap-сервера, устанавливаем соединение указав логин и пароль. Выбираем для поиска писем папку «INBOX». Ищем непрочитанные сообщения, флаг «UNSEEN» с адреса «bridges@torproject.org». В цикле итерируемся по найденным сообщениям. С помощью библиотеки email конвертируем сообщение в более удобный для разбора вид. Получаем дату и время сообщения. Здесь же необходимо получить текущую дату. Следует указать на тот факт, что дата в сообщениях указана по GMT. То есть, требуется получить дату отправки нашего сообщения на сервер Tor, а также текущую дату с данным смещением.
Далее сравниваем даты отправленного нами сообщения и полученного сообщения от сервера. Если дата сообщения от сервера и дата отправки совпадают, проверяем время сообщения. Оно должно быть больше, чем время отправки нашего сообщения серверу. Если все условия удовлетворены, итерируемся по сообщению и забираем из него имя вложения, в нашем случае, вложение это QR-код и сохраняем на диск в папку со скриптом. Возвращаем из функции имя сохраненного вложения.
Если попытка получения почты была неудачной, возвращаем False.
def mail_read(send_time):
with IMAPClient(host="imap.gmail.com") as client:
client.login(config('login'), config("pwd"))
client.select_folder('INBOX')
messages = client.search(["UNSEEN", ['FROM', 'bridges@torproject.org']])
for uid, message_data in client.fetch(messages, "RFC822").items():
email_message = email.message_from_bytes(message_data[b"RFC822"])
mail_date = datetime.strptime(email_message.get("Date"), "%a, %d %b %Y %H:%M:%S %z").date()
mail_time = datetime.strptime(email_message.get("Date"), "%a, %d %b %Y %H:%M:%S %z").time()
now_date = datetime.utcnow().date()
if mail_date == now_date and mail_time > send_time:
for ct in email_message.walk():
if filename := ct.get_filename():
with open(filename, 'wb') as file:
file.write(ct.get_payload(decode=True))
return filename
return False
Копирование настроек torrc
Если мы получили письмо, а также извлекли мосты из QR-кода, необходимо заменить их в файле настроек tor. Но, прежде чем это сделать, сделаем резервную копию. Вдруг, что-то пойдет не так. Поэтому, создаем функцию copy_config(), которая будет копировать файл настроек в точно такой же, с расширением «.bak».
def copy_config():
try:
shutil.copy2("/etc/tor/torrc", "/etc/tor/torrc.bak")
return True
except Exception as e:
print(e)
return False
Обновление файла настроек
После того, как мы сделали копию настроек, открываем файл на чтение. Считываем его построчно и добавляем в список, пропуская строки, которые начинаются на «Bridge» и «UseBridges», так как мы добавим вместо них новые. После этого добавляем полученные мосты в получившийся список, а также строку «UseBridges». Сохраняем полученный список в файл настроек.
def read_write_config(bridges):
with open("/etc/tor/torrc", "r", encoding="utf-8") as torrc:
new_torrc = [x.strip() for x in torrc.readlines() if not x.startswith(("Bridge", "UseBridges")) and x.strip()]
new_torrc.extend([f'Bridge {bridge}' for bridge in bridges])
new_torrc.append("UseBridges 1")
with open("/etc/tor/torrc", "w", encoding="utf-8") as n_torrc:
n_torrc.write("\n".join(new_torrc))
Функция main
Так как код будет менять настройки системной службы, требуется, чтобы скрипт был запущен из под суперпользователя. Для начала проверяем, так ли это. Если нет, сообщаем пользователю и завершаем работу. Также, если ОС не Linux, завершаем работу скрипта.
Затем отправляем почту и получаем время отправки. Делаем паузу на 20 секунд. Затем запускаем бесконечный цикл, который будет периодически проверять почту на наличие необходимого нам письма с ответом. Тут я не уверен во времени ожидания до следующего запроса содержимого ящика. Думаю, что время нужно увеличить с 20 на более высокое значение. Так как обращаться к серверу Google слишком часто тоже не стоит.
После того, как письмо будет получено, считываем данные из QR-кода, делаем копию файла настроек, обновляем его и сохраняем новые значения. После этого перезапускаем службу и выводим в терминал ее статус.
def main():
if platform.system() == "Linux":
if not getuid() == 0:
exit('\n[!] Запустите скрипт с правами суперпользователя!\n')
print("Отправка письма")
send_time = mail_send()
print("Письмо отправлено")
print(f"\rПолучаем почту", end="")
time.sleep(20)
i = True
count_loop = 0
while i:
if filename := mail_read(send_time):
print(f"\rПочта получена", end="")
print("\nQR-код сохранен")
if bridges := qr_read(filename):
print(f"Мосты получены:\n {bridges}")
print("Делаем резервную копию torrc")
if copy_config():
print("Резервная копия torrc сохранена\nМеняем мосты")
read_write_config(bridges)
print("Мосты torrc изменены")
subprocess.call("sudo /etc/init.d/tor restart", shell=True)
subprocess.call("/etc/init.d/tor status", shell=True)
else:
print("\nНе удалось прочитать мосты из QR-кода")
i = False
if count_loop == 4:
exit("\nНе удалось получить сообщение")
count_loop += 1
time.sleep(20)
else:
exit('\n[!] Данный скрипт работает только в ОС Linux\n')
if __name__ == "__main__":
main()
В общем, получилось то, что получилось. Тестирование данного скрипта затруднено тем, что получать письма с мостами можно раз в 3 часа. И, если что-то пошло не так, приходиться ждать следующего раза.
Тем не менее, скрипт отрабатывает успешно. И в итоге мы получаем такой вывод:
"""
pip install python-decouple opencv-python pyzbar IMAPClient
"""
import email
import platform
import re
import shutil
import smtplib
from socket import gaierror
import subprocess
import time
from datetime import datetime
from os import getuid
import cv2
from decouple import config
from imapclient import IMAPClient
from pyzbar.pyzbar import decode
def qr_read(path):
try:
if img := decode(cv2.imread(path)):
briges = re.sub("['\[\]]", "", img[0][0].decode('utf-8')).split(", ")
return briges if briges else False
return False
except TypeError:
return False
def mail_send():
try:
s = smtplib.SMTP_SSL("smtp.gmail.com", 465)
s.login(config("login"), config("pwd"))
message = "\r\n".join([
f"From:{config('login')}",
f"To: bridges@torproject.org",
f"Subject: ",
"",
'"get transport obfs4"'])
s.sendmail(config('login'), "bridges@torproject.org", message)
s.quit()
return datetime.utcnow().time()
except gaierror:
exit('Ошибка соединения')
except smtplib. SMTPAuthenticationError:
exit("Ошибка аутентификации. Проверьте логин или пароль")
def mail_read(send_time):
with IMAPClient(host="imap.gmail.com") as client:
client.login(config('login'), config("pwd"))
client.select_folder('INBOX')
messages = client.search(["UNSEEN", ['FROM', 'bridges@torproject.org']])
for uid, message_data in client.fetch(messages, "RFC822").items():
email_message = email.message_from_bytes(message_data[b"RFC822"])
mail_date = datetime.strptime(email_message.get("Date"), "%a, %d %b %Y %H:%M:%S %z").date()
mail_time = datetime.strptime(email_message.get("Date"), "%a, %d %b %Y %H:%M:%S %z").time()
now_date = datetime.utcnow().date()
if mail_date == now_date and mail_time > send_time:
for ct in email_message.walk():
if filename := ct.get_filename():
with open(filename, 'wb') as file:
file.write(ct.get_payload(decode=True))
return filename
return False
def copy_config():
try:
shutil.copy2("/etc/tor/torrc", "/etc/tor/torrc.bak")
return True
except Exception as e:
print(e)
return False
def read_write_config(bridges):
with open("/etc/tor/torrc", "r", encoding="utf-8") as torrc:
new_torrc = [x.strip() for x in torrc.readlines() if not x.startswith(("Bridge", "UseBridges")) and x.strip()]
new_torrc.extend([f'Bridge {bridge}' for bridge in bridges])
new_torrc.append("UseBridges 1")
with open("/etc/tor/torrc", "w", encoding="utf-8") as n_torrc:
n_torrc.write("\n".join(new_torrc))
def main():
if platform.system() == "Linux":
if not getuid() == 0:
exit('\n[!] Запустите скрипт с правами суперпользователя!\n')
print("Отправка письма")
send_time = mail_send()
print("Письмо отправлено")
print(f"\rПолучаем почту", end="")
time.sleep(20)
i = True
while i:
if filename := mail_read(send_time):
print(f"\rПочта получена", end="")
print("\nQR-код сохранен")
if bridges := qr_read(filename):
print(f"Мосты получены:\n {bridges}")
print("Делаем резервную копию torrc")
if copy_config():
print("Резервная копия torrc сохранена\nМеняем мосты")
read_write_config(bridges)
print("Мосты torrc изменены")
subprocess.call("sudo /etc/init.d/tor restart", shell=True)
subprocess.call("/etc/init.d/tor status", shell=True)
else:
print("\nНе удалось прочитать мосты из QR-кода")
i = False
time.sleep(20)
else:
exit('\n[!] Данный скрипт работает только в ОС Linux\n')
if __name__ == "__main__":
main()
А на этом, все. Спасибо за внимание. Надеюсь, данная информация будет вам полезна.