Pull to refresh

Опыт маскировки OpenVPN-туннеля с помощью obfsproxy

Reading time 9 min
Views 104K
Примечание: приведённая в статье информация во многом устарела, и предназначена скорее для общего ознакомления. Сейчас можно попробовать использовать утилиты вроде ptproxy для создания туннеля с помощью любого актуального pluggable transport для Tor.

Преамбула


В связи с наметившимися тенденциями решил я обфусцировать свой скромный OpenVPN-туннель, просто чтобы набить руку — мало ли пригодится…

Дано: дешевая VPS с белым IP, работающая под Ubuntu Trusty Server Edition и служащая OpenVPN сервером.
Требуется: по-возможности скрыть OpenVPN туннель, желательно без изобретения велосипедов.

obfsproxy


Поскольку на VPS уже была поднята Tor-нода, мысль об obfsproxy пришла сама собой. Как оказалось, утилита действительно умеет притворяться SOCKS-proxy, а значит, обфусцировать не только соединения Tor, но и практически произвольный трафик. Пошарившись по Сети, я нашел несколько мануалов, но многие были либо устаревшими, либо выпускали из виду необходимые подробности.
Важно! Версии.
  • OpenVPN версий младше 2.3.4 содержит баг, мешающей нормальной работе с SOCKS прокси. Если вы хотите использовать режим socks, убедитесь, что у вас установлена подходящая версия OpenVPN.
  • Судя по всему, obfsproxy до сих пор не включен в репозиторий Tor, а в репозиториях Debian/Ubuntu могут лежать старые версии без подержки scramblesuit. В этом случае obfsproxy нужно устанавливать с помощью утилиты pip (пакет python-pip):
     pip install obfsproxy
    

    По окончании установки убедитесь, что используется версия obfsproxy с поддержкой scramblesuit:
     obfsproxy scramblesuit -h
    

  • Версию obfsproxy для Windows можно извлечь из пакета Tor Browser, найдя подкаталог Tor\PluggableTransports.


Из man obfsproxy можно почерпнуть следующее
 obfsproxy [logging_options] [--data-dir path] transport [--dest destination] [--password BASE32PASS] mode listen_addr

Чуть подробнее:
  • --data-dir statepath — каталог для размещения временных данных. Необходим, если obfsproxy работает не в managed режиме.
  • transport — выбирает метод обфускации трафика и режим его передачи. Нас интересуют следующие транспорты:
    • scramblesuit — сравнительно новый метод, который мы и будем использовать.
    • dummy — отладочный метод, при котором трафик передаётся как есть, без обфускации.

  • mode — выбирает режим работы прокси. Нас интересуют следующие режимы:
    • server — принимает клиентские соединения, деобфусцирует и перенаправляет на адрес destination. Этот режим пригодится на стороне сервера.
    • client — прозрачное проксирование: принимает данные, обфусцирует и отправляет серверу по адресу destination. То, что мы будем использовать на клиентской стороне.
    • socks — имитация socks-прокси.

    Разница между режимами client и socks в том, что в первом случае адрес удаленного хоста, к которому пытается подключиться целевое приложение, должен быть заменен на адрес opbfsproxy-клиента, тогда как во втором случае целевое приложение использует obfsproxy как SOCKS-прокси. Для наших целей подойдёт и client.
  • --dest destination — для сервера: задаёт пару адрес-порт, куда будут перенаправляться деобфусцированные соединения. Для клиента: задаёт адрес obfsproxy сервера.
  • --password BASE32PASS — задание ключа(pre-shared key) для метода scramblesuit. Ключ должен быть строкой длиной не менее 20 символов, закодированной в base32, и совпадать на клиенте и на сервере. Подходящий ключ можно сгенерировать так:
    python -c "import os,base64; print base64.b32encode(os.urandom(20))"
    

  • --password-file filepath — альтернативный метод задания ключа для метода scramblesuit, на случай если вы не хотите писать pre-shared key в открытом виде.
  • log_options включает в себя следующие ключи:
    • --log-file path — задает расположение журнала.
    • --log-min-severity level — События какого уровня (debug, info, warning, error) записывать.
    • --no-safe-logging — по умолчанию obfsproxy вырезает адреса из лога, но данный ключ позволяет изменить это поведение.
    • --no-log — отключает ведение журнала совсем (по умолчанию).


Сам VPN-сервер можно либо спрятать за obfsproxy полностью (заставив слушать 127.0.0.1), либо оставить его на внешнем сетевом интерфейсе на случай отказа obfsproxy. Я выбрал второй вариант как более надежный.
Таким образом, для запуска obfsproxy на серверной стороне достаточно выполнить что-то вроде:
obfsproxy --log-file /var/log/obfsproxy-openvpn.log --log-min-severity info scramblesuit --password BASE32LONGPASS --dest 1.2.3.4:1800 server 1.2.3.4:81

где 1.2.3.4 — внешний IP хоста, 1800 — порт, который слушает OpenVPN, 81 — порт, который будет слушать obfsproxy.
Серьезным недостатком является неумение obfsproxy запускаться в режиме демона, но это можно обойти.

Запуск на клиенте практически идентичен серверу:
obfsproxy scramblesuit --password BASE32LONGPASS --dest 1.2.3.4:81 {client|socks} 127.0.0.1:81

где 1.2.3.4:81 — адрес, который слушает obfsproxy-сервер, 127.0.0.1:81 — адрес, который слушает клиент.

Настройка OpenVPN


Важно: OpenVPN должен использовать протокол TCP.
Если мы используем режим client, то просто заменяем адрес удаленного сервера в конфигурационном файле openvpn на адрес локального obfsproxy:
#remote 1.2.3.4 1800 #заменяем старый адрес сервера 
remote 127.0.0.1 81 #вместо этого пытаемся подключиться к клиентской части obfsproxy

Режим socks
Для работы в режиме socks настраиваем OpenVPN немножко по-другому:
#remote 1.2.3.4 1800 #заменяем старый адрес сервера 
remote 1.2.3.4 81 #на адрес obfsproxy-сервера (важно! иначе работать не будет)
socks-proxy-retry
socks-proxy 127.0.0.1 81 #пытаемся задействовать socks proxy

Учитывая необходимость заменять адрес OpenVPN-сервера на адрес obfsproxy-сервера, я не вижу особой пользы от режима прокси — приложения, в которых адреса подключения зашиты намертво, так все равно не запроксируешь.

Если openvpn-трафик блокируется лишь иногда (например, только в рабочей сети), можно использовать следующий прием. В файле конфигурации OpenVPN запишем следующее:
<connection>
    remote 1.2.3.4 1800 #сначала пытаемся подключиться напрямую
</connection>
<connection>
    remote 127.0.0.1 81 #если не удалось, подключаемся через obfsproxy
</connection>

OpenVPN будет пытаться задействовать каждую секцию <connection> поочередно, пока не произведет успешное подключение.

Что касается запуска obfsproxy, к сожалению, сам OpenVPN не поддерживает pre-connect скрипты (запуск команды перед попыткой подключения). Вы можете попытаться установить один из неофициальных патчей, запускать obfsproxy вручную, либо воспользоваться одним из следующих методов.

openvpn-gui для Windows


Если вы используете обертку openvpn-gui для подключения вручную (или автоматически с ключом --connect), то проще всего будет создать в папке config файл XXXXXX_pre.bat, где XXXXXX — имя файла конфигурации (без расширения .ovpn). Этот файл будет автоматически выполнен перед тем как openvpn-gui попытается подключиться. Содержимое файла будет выглядеть примерно так:
start "window title" /MIN "%USERPROFILE%\Tor Browser\Tor\PluggableTransports\obfsproxy.exe" --log-min-severity info --data-dir "%TEMP%\obfs-openvpn" scramblesuit --password-file obfsproxy.key --dest 1.2.3.4:81 client 127.0.0.1:81

  • window title — заголовок окна, необходим.
  • %USERPROFILE%\Desktop\Tor Browser\Tor\PluggableTransports\obfsproxy.exe — путь к исполняемому файлу obfsproxy. По умолчанию Tor Browser ставится на рабочий стол, хотя это и не обязательно.
  • %TEMP%\obfs-openvpn — путь для размещения файлов состояния obfsproxy.
  • obfsproxy.key — путь к файлу с ключом, если вы его используете.
  • 1.2.3.4:81 — адрес, который слушает серверная часть obfsproxy.
  • 127.0.0.1:81 — адрес, который будет слушать клиентская часть.

Использование команды start (без ключа /wait она работает как аналог оператора & в shell) необходимо, так как openvpn-gui ожидает завершения pre-up скрипта перед попыткой подключения. Недостатком этого метода является консольное окно, постоянно висящее в фоне. При желании его можно спрятать утилитой hidcon или аналогичной.

obfsproxy как сервис


Если OpenVPN запускается как сервис, то имеет смысл организовать запуск obfsproxy таким же образом.
Создать сервис можно утилитой srvany от MS или какой-либо альтернативной утилитой, вроде NSSM. Вопрос запуска приложения как службы Windows выходит за рамки статьи, так что я просто укажу, что такой подход позволит задавать зависимости от и для других сервисов, тем самым гарантируя что obfsproxy будет доступен в момент запуска OpenVPN.

Этот подход можно совместить с предыдущим, используя bat-файл, запускаемый openvpn-gui, для запуска созданного сервиса командой net start.

Влияние на скорость


Последняя проверка проводилась 7 февраля 2016 посредством старого доброго speedtest.net, используя один и тот же сервер в Лондоне (OpenVPN-сервер хостится в Латвии, клиент — в европейской части России) с клиентской машины под Windows 7. Измерения производились в десятикратной повторности. Из результатов следует, что как минимум в моем случае значительного влияния на скорость obfsproxy не оказывает. Так что, если до вашего VPN-сервера хороший канал, то хуже не будет.

Режим Ping, ms Download, MB/s Upload, MB/s
direct среднее 56 20,18 21,03
коэфф. вариации 3,6% 0,6% 1,2%
openvpn среднее 56 20,21 21,29
коэфф. вариации 2,3% 1,0% 1,0%
openvpn+obfsproxy среднее 55 20,24 21,15
коэфф. вариации 4,0% 0,73% 1,34%


Запуск obfs-proxy как init.d сервиса


init.d/obfs-openvpn
#! /bin/sh
### BEGIN INIT INFO
# Provides: obfsproxy-openvpn
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: obfsproxy wrapper for openvpn
# Description: This file starts up obfsproxy configured to process obfuscated openvpn client connections.
### END INIT INFO

# Author: Vindicar <the.vindicar@gmail.com>

# Do NOT «set -e»

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH="/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin"
DESC=«obfsproxy wrapper for openvpn»
NAME=obfs-openvpn
DAEMON="/usr/local/bin/obfsproxy"

OBFSPROXY_KEYFILE="/etc/obfsproxy-openvpn.key"
SERVER_IP=?.?.?.? #<<< TODO: insert server address here
OPENVPN_PORT=443 #port 443 due to use of sslh
OBFSPROXY_PORT=81

DAEMON_ARGS="--log-file /var/log/obfs-openvpn.log --log-min-severity warning \
--data-dir /tmp/obfs-openvpn \
scramblesuit --password-file $OBFSPROXY_KEYFILE \
--dest $SERVER_IP:$OPENVPN_PORT server $SERVER_IP:$OBFSPROXY_PORT"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

# 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

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
# and status_of_proc is working.
. /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 --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|| return 1
start-stop-daemon --start --background --quiet --make-pidfile --pidfile $PIDFILE --exec $DAEMON — \
$DAEMON_ARGS \
|| return 2
# Add code here, if necessary, that waits for the process to be ready
# to handle requests from services started subsequently which depend
# on this one. As a last resort, sleep for some time.
}

#
# Function that stops the daemon/service
#
do_stop()
{
# Return
# 0 if daemon has been stopped
# 1 if daemon was already stopped
# 2 if daemon could not be stopped
# other if a failure occurred
# EDIT: removed --name parameter since s-s-d matches it against process name which is 'python'
# will be relying on the pidfile to stop it.
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
RETVAL="$?"
[ "$RETVAL" = 2 ] && return 2
# Wait for children to finish too if this is a daemon that forks
# and if the daemon is only ever run from this initscript.
# If the above conditions are not satisfied then add some other code
# that waits for the process to drop all resources that could be
# needed by services started subsequently. A last resort is to
# sleep for some time.
#start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
#[ "$?" = 2 ] && return 2
# Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE
return "$RETVAL"
}

#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
#
# If the daemon can reload its configuration without
# restarting (for example, when it is sent a SIGHUP),
# then implement that here.
#
start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE
return 0
}

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 $?
;;
#reload|force-reload)
#
# If do_reload() is not implemented then leave this commented out
# and leave 'force-reload' as an alias for 'restart'.
#
#log_daemon_msg «Reloading $DESC» "$NAME"
#do_reload
#log_end_msg $?
#;;
restart|force-reload)
#
# If the «reload» option is implemented then remove the
# 'force-reload' alias
#
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
Tags:
Hubs:
+32
Comments 37
Comments Comments 37

Articles