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

327 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Прозрачный прокси контейнера через Shadowsocks и tun2socks
У меня есть Docker-контейнер Audiobookshelf, в котором я слушаю аудио-книги и подкасты. Он сам скачивает подкасты
из интернета и это очень удобно (все нужные мне подкасты в одном месте). Работал контейнер на домашнем NAS Synology
(в нем есть Docker Station с веб-интерфейсов). Книг у меня в "библиотеке" уже более тысячи, а подкастов и того больше,
так что домашний NAS с доступом из интернет -- идеальное решение.
Все отлично работало и подкасты обновлялись, пока не настало DPI. Где-то в первой декаде января 2026 подкасты перестали
обновляться. Проверки показали, что это из-за блокировок на уровне провайдера. Mp3-файлы файлы размещены на
DigitalOcean, и заблокирован именно он. Audiobookshelf получает ссылки через RSS-фиды, видит новые эпизоды, и зависает
при скачивании.
## Как решить проблему DPI?
Как все работало до блокировок:
### Старая схема
```text
Интернет
┌─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-файлы
через прокси. При этом прослушивание книг и подкастов должно работать по старой схеме, без изменений.
Как-то так:
```text
Интернет для upload Интернет для прослушивания
▲ ▲
│ │
│ │
┌─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 у меня было:
```yml
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
Теперь у нас будет три контейнера (и определённая зависимость между ними):
```yml
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] Privoxy — HTTP-прокси для Node.js, форвардит через 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, использующий Privoxy для доступа к интернету
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://` и до `@`). Например, командой
в терминале:
```shell
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`). Поэтому контейнеру нужны права
и доступ к этому устройству. Это достигается через:
```yml
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun
```
На Synology (да и многих Linux) TUN-интерфейс `/dev/net/tun` обычно закрыт. Нужно будет разрешить доступ к нему:
```bash
# Проверяем, что 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 плохо работает с proxy, ломаются редиректы,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, что проект "зелененький" и все контейнеры тоже
"зелененькие". Ну или выполнить:
```bash
sudo docker ps
```
И увидим что-то типа:
```text
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:
```bash
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 подкаста "Аэростат" с альбомом Аквариума "Странные Новости с
Далёкой Звезды" (2026 год, очень рекомендую послушать). Этот подкаст размещен на DigitalOcean, так что если
он скачался, значит туннель работает и обход блокировок провайдера сработал.
Проверить что метаданные из iTunes тоже подгружаются, можно проверить только настроив автообновление подкастов
в интерфейсе Audiobookshelf (например через ять минут). К получение метаданных происходит "под капотом"
и иначе никак не диагностируется.
Должно все работать. Но если не работает, то "курите логи" контейнера `audiobookshelf`:
```bash
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-фид c ссылкой на внутренний сервис. Например: `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 proxy находился в 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`), при котором приложение не знает о прокси вообще.