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

323 lines
25 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
[![Static Badge](https://img.shields.io/badge/ПОЛЕЗНО-99.9%-blue)](#)
[![Static Badge](https://img.shields.io/badge/СЛОЖНОСТЬ-7/10-orange)](#)
[![Static Badge](https://img.shields.io/badge/ОРИГИНАЛ-git.cube2.ru-green)](https://git.cube2.ru/erjemin/doc_memo/src/branch/master/docker/docker-proxy-container-via-shadowsocks-and-tun2socks.md)
[![Static Badge](https://img.shields.io/badge/ТИПОГРАФИКА-etpgrf-green)](https://typograph.cube2.ru/)
У меня есть 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
Интернет для скачивания Интернет для прослушивания
▲ ▲
│ │
│ │
┌─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] 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://` и `@`). Например, командой
в терминале:
```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 плохо работает с прокси, ломаются редиректы, 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-фид со ссылкой на внутренний сервис. Например: `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`), при котором приложение не знает о прокси­ровании вообще.