Files
doc_memo/docker/docker-proxy-container-via-shadowsocks-and-tun2socks.md

24 KiB
Raw Blame History

Прозрачный прокси контейнера через Shadowsocks и tun2socks

У меня есть Docker-контейнер Audiobookshelf, в котором я слушаю аудиокниги и подкасты. Он сам скачивает подкасты из интернета, и это очень удобно (все нужные мне подкасты в одном месте). Работал контейнер на домашнем NAS Synology (в нем есть Docker Station с веб-интерфейсом). Книг у меня в "библиотеке" уже более тысячи, а подкастов и того больше, так что домашний NAS с доступом из интернета — идеальное решение.

Все отлично работало и подкасты обновлялись, пока не настал DPI. Где-то в первой декаде января 2026 года подкасты перестали обновляться. Проверки показали, что это из-за блокировок на уровне провайдера. Mp3-файлы размещены на DigitalOcean, и заблокирован именно он. Audiobookshelf получает ссылки через RSS-фиды, видит новые эпизоды и зависает при скачивании.

Как решить проблему DPI?

Как все работало до блокировок:

Старая схема

                                                             Интернет
                                                                ▲
                                                                │
                                                                │
┌─Docker (на Synology)──────┐     скачивание подкастов     ┌────┴─────┐
│      Audiobookshelf       ├─────────────────────────────►│  Роутер  │
└──────────────────┬────────┘                              └────▲─────┘
                   │                                            │
                   │ для прослушивания                          │
                   │ (порт 8000)                                │
                   │                                            │
┌─Host (Orange Pi)─▼────────┐     для прослушивания c SSL       │
│        Nginx Proxy        ├───────────────────────────────────┘
└───────────────────────────┘

Контейнер Audiobookshelf на Synology работает по 8000 порту. Nginx на Orange Pi 5 Plus проксирует на этот порт на IP-адресе Synology, добавляет SSL и обновляет сертификат Let's Encrypt (на самом деле у меня для этой цели используется Traefik на k3s и все еще проходит через VIP-адрес Keepalive, но для простоты я опускаю эти детали). На роутере настроен проброс 443-порта на Orange Pi. Внешний DNS настроен на IP-адрес роутера (A- и AAAA-записи), и так я могу слушать подкасты из любой точки мира.

Скачивание подкастов внутри Audiobookshelf идет напрямую. Контейнер сразу обращается к Роутеру и пытается скачать mp3-файл. И это ему не удаётся из-за DPI.

Новая схема

К счастью, проверки показали, что у провайдеров (даже у российских) нет блокировок DigitalOcean. А значит, эпизоды подкастов можно скачать на VPS/VDS-виртуалке, и нужно только настроить Audiobookshelf так, чтобы он скачивал mp3-файлы через прокси. При этом прослушивание книг и подкастов должно работать по старой схеме, без изменений.

Как-то так:

                                 Интернет для скачивания      Интернет для прослушивания
                                          ▲                          ▲
                                          │                          │
                                          │                          │
                   ┌─VPS/VDS (Hosting)────────────┐                  │ для прослушивания
                   │   VPN (Shadowsocks Server)   │                  │ (SSL)
                   └──────────────────────▲───────┘                  │
                                          ╚═════════════════════╗    │
                                                             ┌──╨────┴──┐
                                                             │  Роутер  │
                                                             └──▲────▲──┘
                                                                ║    │
                                                     для upload ║    └──────────────┐
                                                  (VPN-тоннель) ║                   │
···Docker Compose (на Synology)································ ║ ··············    │
:                                                               ║              :    │
:   ┌────────────────┐      ┌───────────────┐         ┌─────────╨──────────┐   :    ▲
:   │ Audiobookshelf ├─────►│   tun2socks   ├────────►│ shadowsocks/SOCKS5 │   :    │
:   └────────────────┘      └───────┬───────┘         └────────────────────┘   :    │
:                                   │ для прослушивания                        :    │
··································· │ ··········································    │
                                    │ (порт 8000)                                   ▲
                 ┌─Host (Orange Pi)─▼────────┐        для прослушивания c SSL       │
                 │        Nginx Proxy        ├──────────────────────────────────────┘
                 └───────────────────────────┘

Почему так сложно?

  • Во-первых, на VPS/VDS, где я держу свои проекты, уже был настроен Shadowsocks-сервер (Outline), не для обхода блокировок, а для безопасного доступа и администрирования (а еще для того, чтобы посещать Госуслуги и другие ресурсы, находясь в поездках за границей). Поэтому было логично использовать его.
  • Во-вторых, для Docker не существует полноценного VPN-клиента Shadowsocks. Есть только прокси-клиенты (ss-local), которые предоставляют SOCKS5-интерфейс.
  • При этом — и это в-третьих — Audiobookshelf не умеет работать с SOCKS5. Ему нужен настоящий HTTP, причем из-за встроенной в Audiobookshelf защиты от SSRF-атак он не может работать даже с HTTP-прокси, который находится в Docker-сети (например, Privoxy). Ему нужно сделать прозрачную сеть через TUN-интерфейс — тогда он будет доволен.
  • Для этого, в-четвертых, нужно сделать так, чтобы весь трафик контейнера Audiobookshelf автоматически шел через единственный контейнер (tun2socks), который будет форвардить и http, и https через Shadowsocks (для скачивания подкастов) и отдавать http-трафик на порт 8000 для прослушивания через Nginx. Именно такая схема не будет ломать работу SSRF-фильтра и защитит от этого типа атаки (можно было, конечно, откатить Audiobookshelf до древней версии 2.6.0, где дыра SSRF еще была, но это не самое безопасное решение, да и на DockerHub уже нет этой версии).

Старый манифест Docker

В Docker Station на Synology у меня было:

version: "3.8"
services: audiobookshelf:
    image: advplyr/audiobookshelf:latest
    restart: always
    ports:
        - 8000:80
    volumes:
        - ./config:/config
        - ./metadata:/metadata
        - /volume1/music/_podcasts:/podcasts
        - /volume1/music/_audio_books:/audiobooks
    environment:
        - TZ=Europe/Moscow

Тут, вроде, всё просто. На Synology есть папка /volume1/music/ уже с завода. В ней я создал папки _podcasts и _audio_books для хранения книг и подкастов и смонтировал эти папки в контейнер. Также добавил папки config и metadata для хранения конфигурации и метаданных. Эти папки создаются при первом запуске контейнера и будут расположены рядом с compose.yml (в каталоге /volume1/docker/audiobookshelf/). Папка /volume1/docker/ тоже будет создана при установке приложения Docker Station на Synology; нам нужно только создать каталог audiobookshelf/ и выбрать его при создании проекта в Docker Station.

Порт 8000 проброшен наружу, и Nginx на хосте Orange Pi 5 Plus проксирует на этот порт, добавляет SSL и так далее.

Новая схема и манифест для Docker Station

Теперь у нас будет три контейнера (и определённая зависимость между ними):

version: "3.8"

services:
  # [1] Shadowsocks клиент для подключения к Outline/VPS (предоставляет SOCKS5 на порту 1080)
  ss-ru:
    image: shadowsocks/shadowsocks-libev
    container_name: ss-ru
    restart: unless-stopped
    command: >
      ss-local
      -s [IP-адрес Outline/VPS]
      -p [порт Outline/VPS]
      -k [пароль Outline/VPS]
      -m [метод шифрования]
      -l 1080
      -b 0.0.0.0

  # [2] tun2socks — прозрачный прокси для Audiobookshelf, форвардит через ss-ru
  tun2socks:
    image: xjasonlyu/tun2socks
    container_name: tun2socks
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    ports:
      - "8000:80"
    command: >
      -device tun://tun0
      -proxy socks5://ss-ru:1080
    depends_on:
      - ss-ru

  # [3] Audiobookshelf, использующий сетевой стек tun2socks
  audiobookshelf:
    image: advplyr/audiobookshelf:latest
    container_name: audiobookshelf
    restart: always
    network_mode: "service:tun2socks"
    volumes:
      - ./config:/config
      - ./metadata:/metadata
      - /volume1/music/_audio_books:/audiobooks
      - /volume1/music/_podcasts:/podcasts
    environment:
      - TZ=Europe/Moscow
    depends_on:
      tun2socks:
        condition: service_started

Что тут происходит:

Первым стартует контейнер ss-ru. Он ни от кого не зависит. Он подключается к моему Outline-серверу и предоставляет SOCKS5-прокси на порту 1080.

Где брать параметры для подключения к Outline/VPS? В панели Outline Manager "Add new Key". Он выдаст строку вида: ss://[зашифрованный-base64-ключ--метод-шифрования:пароль]@[IP-адрес Outline/VPS]:[порт Outline/VPS]/?outline=1. Параметры [IP-адрес Outline/VPS] и [порт Outline/VPS] нужно взять непосредственно из этой строки. Метод шифрования и пароль нужно расшифровать из первой части строки (часть строки между ss:// и @). Например, командой в терминале:

echo "Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpDYVF6OERMRDVHd09meTRvVVk1TmxK" | base64 -d

Получим строку вида chacha20-ietf-poly1305:пароль. Таким образом, [метод шифрования] — это chacha20-ietf-poly1305, а [пароль Outline/VPS] — это пароль.

Вторым стартует контейнер tun2socks. Он зависит от ss-ru, так что гарантированно запустится после него. Он использует ss-ru как SOCKS5-прокси и создает TUN-интерфейс tun0 внутри контейнера. Также он предоставляет HTTP-интерфейс на порту 80 внутри нашего проекта Docker Station (с него будет отвечать Audiobookshelf), который проброшен наружу как 8000 (его будет проксировать Nginx на хосте Orange Pi 5 Plus).

Контейнер tun2socks использует системный TUN-интерфейс Synology (/dev/net/tun). Поэтому контейнеру нужны права и доступ к этому устройству. Это достигается через:

    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun

На Synology (да и на многих Linux-системах) TUN-интерфейс /dev/net/tun обычно закрыт. Нужно будет разрешить доступ к нему:

# Проверяем, что TUN доступен
ls /dev/net/tun
# Если команда выдаёт "/dev/net/tun: no such device", то нужно загрузить модуль ядра
sudo modprobe tun
# И снова проверить, что устройство появилось
ls /dev/net/tun
# Должно вывести: "/dev/net/tun"
Заметим
Я пробовал другие решения: (1) Privoxy — Проблемы: axios внутри Audiobookshelf плохо работает с прокси, ломаются редиректы, SSRF-фильтр; сами RSS загружаются, но SSRF будет блокировать запросы к iTunes, а значит, не будут получаться метаданные, а без метаданных и mp3-файлы не скачиваются. (2) redsocks — Проблемы: нестабильный iptables redirect (на Synology это вообще не работает, похоже), падение самого redsocks, сложная настройка NAT внутри Docker.

Третьим стартует контейнер audiobookshelf. В нём почти всё по-старому. Но теперь он зависит от tun2socks, так что гарантированно запустится после него. Кроме того, у него нет проброса портов 8000:80 (они у нас теперь в контейнере tun2socks). Вместо этого у него network_mode: "service:tun2socks". В целом, network_mode: "service:..." — это мощная, но специфичная функция Docker, которая 'склеивает' сетевые стеки контейнеров в один, а это значит, что контейнер audiobookshelf будет использовать сетевой стек контейнера tun2socks и в результате весь трафик будет идти через TUN-интерфейс. Дальше уже сам tun2socks определит, что с ним делать. Если трафик пришел снаружи, с порта 8000, то он попадёт в Audiobookshelf, как и раньше, а если же сам Audiobookshelf начнет что-то скачивать, то запрос уйдет через TUN-интерфейс, попадет в tun2socks, который перенаправит его через SOCKS5-прокси ss-ru на Outline/VPS (на внешнем хостинге).

Запуск и проверка

Конечно, первым делом нужно проверить в интерфейсе Docker Station, что проект "зелененький" и все контейнеры тоже "зелененькие". Ну или выполнить:

sudo docker ps

И увидим что-то типа:

CONTAINER ID   IMAGE                           COMMAND                  CREATED   STATUS   PORTS                                   NAMES
8599f59103eb   advplyr/audiobookshelf:2.28.0   "tini -- node index.…"   16s ago   Up 17s                                           audiobookshelf
038ea5878848   xjasonlyu/tun2socks             "/entrypoint.sh -dev…"   16s ago   Up 16s   0.0.0.0:8000->80/tcp, :::8000->80/tcp   tun2socks
3bdb3269ed77   shadowsocks/shadowsocks-libev   "ss-local -s 90.156.…"   16s ago   Up 16s                                           ss-ru

Для проверки того, что тоннель работает и подкасты скачиваются, выполним в терминале на Synology:

sudo docker exec -it audiobookshelf sh -c " node -e \"const https=require('https');const fs=require('fs');const file=fs.createWriteStream('/podcasts/test.mp3');https.get('https://aerostats.getmobileup.com/music/1074.mp3',res=>{res.pipe(file);file.on('finish',()=>console.log('Done'))})\""

Audiobookshelf работает на Node.js, а внутри контейнера нет ни curl, ни wget. Так что придется запустить такой заковыристый js-скрипт для проверки скачивания файла.

В результате мы получим test.mp3 в папке /podcasts/ (а она у нас снаружи контейнера в /volume1/music/_podcasts). Это выпуск №1074 подкаста "Аэростат" с альбомом Аквариума "Странные Новости с Далёкой Звезды" (2024 год, очень рекомендую послушать). Этот подкаст размещен на DigitalOcean, так что если он скачался, значит туннель работает и обход блокировок провайдера сработал.

Проверить, что метаданные из iTunes тоже подгружаются, можно, только настроив автообновление подкастов в интерфейсе Audiobookshelf (например, через пять минут). Но получение метаданных происходит "под капотом", и иначе это никак не диагностировать.

Должно всё работать. Но если не работает, то "курите логи" контейнера audiobookshelf:

sudo docker logs audiobookshelf --tail 100

...и Google и ИИ-ассистенты в помощь. Node.js — это не самая простая среда для отладки.

P.S. Важное замечание о защите Audiobookshelf от SSRF

Во время настройки в логах можно столкнуться с ошибкой вида: Call to 172.25.0.3 is blocked или invalid feed payload. Это не ошибка сети и не проблема RSS. Это встроенная защита Audiobookshelf от SSRF-атак.

Что такое SSRF

SSRF (Server Side Request Forgery) — это тип атаки, когда сервер заставляют делать запросы в свою внутреннюю сеть. Например, злоумышленник может подложить RSS-фид со ссылкой на внутренний сервис. Например: http://127.0.0.1:8080/admin или http://172.25.0.3:5000.

Если сервер загрузит такой URL, атакующий потенциально сможет:

  • читать данные внутренних сервисов
  • получать внутренние токены авторизации
  • обращаться к Docker API
  • сканировать внутреннюю сеть

Поэтому многие современные приложения блокируют такие запросы. И Audiobookshelf не исключение.

Audiobookshelf использует библиотеку ssrf-req-filter. Она проверяет IP-адрес, в который разрешается (резолвится) домен, и блокирует некоторые диапазоны:

  • 127.0.0.0/8
  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16
  • 169.254.0.0/16

То есть любые внутренние адреса, включая адреса Docker-сети.

Когда использовалась схема Audiobookshelf → HTTP Proxy → Shadowsocks, то HTTP-прокси находился в Docker-сети. Например: privoxy:8118 или redsocks:12345.

После DNS-разрешения (резолва) запрос фактически отправлялся на адрес контейнера прокси внутри Docker-сети. Например, 172.25.0.3. И SSRF-фильтр Audiobookshelf это видел и блокировал запрос. Именно поэтому в логах появлялось: Call to 172.25.0.3 is blocked.

Почему HTTP_PROXY часто ломает Audiobookshelf

Audiobookshelf использует библиотеку axios. А он: - плохо работает с редиректами через прокси - иногда разрешает (резолвит) DNS на стороне прокси - иногда разрешает (резолвит) DNS локально

В результате: часть запросов проходит, часть падает, RSS может читаться, но mp3 не скачиваются и так далее.

Опытным путем я определил, что наиболее надёжный способ — сетевой туннель (tun2socks), при котором приложение не знает о проксировании вообще.