add: Прозрачный прокси контейнера через Shadowsocks и tun2socks (fin)

This commit is contained in:
2026-03-06 19:46:00 +03:00
parent e63a980b2b
commit f2f6bb65d2

View File

@@ -1,13 +1,13 @@
# Прозрачный прокси контейнера через Shadowsocks и tun2socks
У меня есть Docker-контейнер Audiobookshelf, в котором я слушаю аудио-книги и подкасты. Он сам скачивает подкасты
из интернета и это очень удобно (все нужные мне подкасты в одном месте). Работал контейнер на домашнем NAS Synology
(в нем есть Docker Station с веб-интерфейсов). Книг у меня в "библиотеке" уже более тысячи, а подкастов и того больше,
так что домашний NAS с доступом из интернет -- идеальное решение.
У меня есть Docker-контейнер Audiobookshelf, в котором я слушаю аудиокниги и подкасты. Он сам скачивает подкасты
из интернета, и это очень удобно (все нужные мне подкасты в одном месте). Работал контейнер на домашнем NAS Synology
(в нем есть Docker Station с веб-интерфейсом). Книг у меня в "библиотеке" уже более тысячи, а подкастов и того больше,
так что домашний NAS с доступом из интернета идеальное решение.
Все отлично работало и подкасты обновлялись, пока не настало DPI. Где-то в первой декаде января 2026 подкасты перестали
обновляться. Проверки показали, что это из-за блокировок на уровне провайдера. Mp3-файлы файлы размещены на
DigitalOcean, и заблокирован именно он. Audiobookshelf получает ссылки через RSS-фиды, видит новые эпизоды, и зависает
Все отлично работало и подкасты обновлялись, пока не настал DPI. Где-то в первой декаде января 2026 года подкасты перестали
обновляться. Проверки показали, что это из-за блокировок на уровне провайдера. Mp3-файлы размещены на
DigitalOcean, и заблокирован именно он. Audiobookshelf получает ссылки через RSS-фиды, видит новые эпизоды и зависает
при скачивании.
## Как решить проблему DPI?
@@ -35,22 +35,22 @@
Контейнер 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-записи) и так я могу
Traefik на k3s и все еще проходит через VIP-адрес Keepalive, но для простоты я опускаю эти детали). На роутере
настроен проброс 443-порта на Orange Pi. Внешний DNS настроен на IP-адрес роутера (A- и AAAA-записи), и так я могу
слушать подкасты из любой точки мира.
Скачивание подкастов внутри Audiobookshelf идет напрямую. Контейнер сразу обращается к Роутеру и пытается скачать
Скачивание подкастов внутри Audiobookshelf идет напрямую. Контейнер сразу обращается к Роутеру и пытается скачать
mp3-файл. И это ему не удаётся из-за DPI.
### Новая схема
К счастью, проверки показали, что у провайдеров (даже у Российских) нет блокировок DigitalOcean. А значит эпизоды
подкастов можно скачать на VPS/VDS виртуалке, и нужно только настроить Audiobookshelf так, чтобы он скачивал mp3-файлы
К счастью, проверки показали, что у провайдеров (даже у российских) нет блокировок DigitalOcean. А значит, эпизоды
подкастов можно скачать на VPS/VDS-виртуалке, и нужно только настроить Audiobookshelf так, чтобы он скачивал mp3-файлы
через прокси. При этом прослушивание книг и подкастов должно работать по старой схеме, без изменений.
Как-то так:
```text
Интернет для upload Интернет для прослушивания
Интернет для скачивания Интернет для прослушивания
▲ ▲
│ │
│ │
@@ -79,19 +79,19 @@ Traefik на k3s и все еще проходдит через VIP-адрес K
Почему так сложно?
- Во-первых, на VPS/VDS, где я держу свои проекты, уже был настроен Shadowsocks-сервер (Outline), Не для обхода
блокировок, а для безопасного доступа и администрирования (а еще для того чтоб посещать Госуслуги и другие ресурсы,
- **Во-первых**, на VPS/VDS, где я держу свои проекты, уже был настроен Shadowsocks-сервер (Outline), не для обхода
блокировок, а для безопасного доступа и администрирования (а еще для того, чтобы посещать Госуслуги и другие ресурсы,
находясь в поездках за границей). Поэтому было логично использовать его.
- Во-вторых, для Docker не существует полноценного VPN-клиента Shadowsocks. Есть только прокси-клиенты (ss-local),
которые предоставляют SOCKS5 интерфейс.
- При этом, и это в-третьих, Audiobookshelf не умеет работать с SOCKS5. Ему нужен настоящий HTTP, причем из-за
встроенной в Audiobookshelf защиты от SSRF атак он не может работать даже с HTTP-прокси, который находится
в Docker сети (например, Privoxy). Ему нужно сделать прозрачную сеть через TUN интерфейс -- тогда он будет
- **Во-вторы**х, для Docker не существует полноценного VPN-клиента Shadowsocks. Есть только прокси-клиенты (ss-local),
которые предоставляют SOCKS5-интерфейс.
- При этом и это **в-третьих** Audiobookshelf не умеет работать с SOCKS5. Ему нужен настоящий HTTP, причем из-за
встроенной в Audiobookshelf защиты от SSRF-атак он не может работать даже с HTTP-прокси, который находится
в Docker-сети (например, Privoxy). Ему нужно сделать прозрачную сеть через TUN-интерфейс тогда он будет
доволен.
- Для этого, в-четвертых, нужно сделать так, чтобы весь трафик контейнера Audiobookshelf автоматически
- Для этого, **в-четвертых**, нужно сделать так, чтобы весь трафик контейнера Audiobookshelf автоматически
шел через единственный контейнер (tun2socks), который будет форвардить и http, и https через Shadowsocks (для
скачивания подкастов) и отдавать http-трафик на порт 8000 для прослушивания через Nginx. Именно такая схема
не будет ломать работу SSRF-фильтра и защитит от этого тип атаки (_можно было, конечно, откатить Audiobookshelf
не будет ломать работу SSRF-фильтра и защитит от этого типа атаки (_можно было, конечно, откатить Audiobookshelf
до древней версии 2.6.0, где дыра SSRF еще была, но это не самое безопасное решение, да и на DockerHub уже нет
этой версии_).
@@ -114,14 +114,14 @@ services: audiobookshelf:
- TZ=Europe/Moscow
```
Тут, вроде все просто. На Synology есть папка `/volume1/music/` уже с завода. В ней я создал папки `_podcasts`
и `_audio_books` для хранения книг и подкастов, и смонтировал эти папки в контейнер. Также добавил папки `config`
и `metadata` для хранения конфигурации и метаданных. Эти папки создадются при первом запуске контейнера, и будут
расположены рядом `compose.yml` (в каталоге `/volume1/docker/audiobookshelf/`... папка `/volume1/docker/` тоже
будет создана при установке приложения Docker Station на Synology... нам нужно только создать каталог `audiobookshelf/`
и и выбрать ее при создании проекта в Docker Station).
Тут, вроде, всё просто. На 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 и так далее.
Порт 8000 проброшен наружу, и Nginx на хосте Orange Pi 5 Plus проксирует на этот порт, добавляет SSL и так далее.
### Новая схема и манифест для Docker Station
@@ -145,7 +145,7 @@ services:
-l 1080
-b 0.0.0.0
# [2] Privoxy — HTTP-прокси для Node.js, форвардит через ss-ru
# [2] tun2socks — прозрачный прокси для Audiobookshelf, форвардит через ss-ru
tun2socks:
image: xjasonlyu/tun2socks
container_name: tun2socks
@@ -162,7 +162,7 @@ services:
depends_on:
- ss-ru
# [3] Audiobookshelf, использующий Privoxy для доступа к интернету
# [3] Audiobookshelf, использующий сетевой стек tun2socks
audiobookshelf:
image: advplyr/audiobookshelf:latest
container_name: audiobookshelf
@@ -182,25 +182,25 @@ services:
Что тут происходит:
*Первым стартует** контейнер `ss-ru`. Он ни от кого не зависит. Он подключается к моему Outline-серверу и предоставляет
**Первым стартует** контейнер `ss-ru`. Он ни от кого не зависит. Он подключается к моему Outline-серверу и предоставляет
SOCKS5-прокси на порту `1080`.
Где брать параметры для подключения к Outline/VPS? В панели Outline Manager "Add new Key". Она предоставит строку вида:
Где брать параметры для подключения к Outline/VPS? В панели Outline Manager "Add new Key". Он выдаст строку вида:
`ss://[зашифрованный-base64-ключ--метод-шифрования:пароль]@[IP-адрес Outline/VPS]:[порт Outline/VPS]/?outline=1`.
Параметры `[IP-адрес Outline/VPS]` и `[порт Outline/VPS]` нужно взять непосредственно из этой строки. Метод шифрования
и пароль нужно расшифровать из первой части строки (часть строки между `ss://` и до `@`). Например, командой
и пароль нужно расшифровать из первой части строки (часть строки между `ss://` и `@`). Например, командой
в терминале:
```shell
echo "Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpDYVF6OERMRDVHd09meTRvVVk1TmxK" | base64 -d
```
Получим строку вида `chacha20-ietf-poly1305:пароль`. Таким образом `[метод шифрования]` — это `chacha20-ietf-poly1305`,
а `[пароль Outline/VPS] — это `пароль`.
Получим строку вида `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).
использует `ss-ru` как SOCKS5-прокси и создает TUN-интерфейс `tun0` внутри контейнера. Также он предоставляет
HTTP-интерфейс на порту 80 внутри нашего проекта Docker Station (с него будет отвечать Audiobookshelf), который
проброшен наружу как 8000 (его будет проксировать Nginx на хосте Orange Pi 5 Plus).
Контейнер tun2socks использует системный TUN-интерфейс Synology (`/dev/net/tun`). Поэтому контейнеру нужны права
и доступ к этому устройству. Это достигается через:
@@ -211,11 +211,11 @@ services:
- /dev/net/tun
```
На Synology (да и многих Linux) TUN-интерфейс `/dev/net/tun` обычно закрыт. Нужно будет разрешить доступ к нему:
На Synology (да и на многих Linux-системах) TUN-интерфейс `/dev/net/tun` обычно закрыт. Нужно будет разрешить доступ к нему:
```bash
# Проверяем, что TUN доступен
ls /dev/net/tun
# Если выдаёт "/dev/net/tun: no such device", то нужно создать устройство
# Если команда выдаёт "/dev/net/tun: no such device", то нужно загрузить модуль ядра
sudo modprobe tun
# И снова проверить, что устройство появилось
ls /dev/net/tun
@@ -224,20 +224,20 @@ ls /dev/net/tun
| Заметим |
| ------- |
| Я пробовал други решения: (1) `Privoxy` -- Проблемы: axios внутри Audiobookshelf плохо работает с proxy, ломаются редиректы,SSRF фильтр, сами RSS загружаются, но SSRF будет блокировать запросы к iTunes, а, значит, не будут получаться метаданные, а без метаданных и mp3-файлы не скачиваются. (2) `redsocks` -- Проблемы: нестабильный iptables redirect (на Synology это вообще не работает, похоже), падение самого redsocks, сложная настройка NAT внутри Docker.
| Я пробовал другие решения: (1) `Privoxy` — Проблемы: axios внутри Audiobookshelf плохо работает с прокси, ломаются редиректы, SSRF-фильтр; сами RSS загружаются, но SSRF будет блокировать запросы к iTunes, а значит, не будут получаться метаданные, а без метаданных и mp3-файлы не скачиваются. (2) `redsocks` — Проблемы: нестабильный iptables redirect (на Synology это вообще не работает, похоже), падение самого redsocks, сложная настройка NAT внутри Docker.
**Третьим стартует** контейнер `audiobookshelf`. В нём почти все по старому. Но теперь он зависит от `tun2socks`,
**Третьим стартует** контейнер `audiobookshelf`. В нём почти всё по-старому. Но теперь он зависит от `tun2socks`,
так что гарантированно запустится после него. Кроме того, у него нет проброса портов `8000:80` (они у нас теперь
в контейнере tun2socks). Вместо этого у него `network_mode: "service:tun2socks"`. В целом, `network_mode:
"service:...` -- это мощная, но специфичная функция Docker, которая 'склеивает' сетевые стеки контейнеров в один,
"service:..."` — это мощная, но специфичная функция Docker, которая 'склеивает' сетевые стеки контейнеров в один,
а это значит, что контейнер `audiobookshelf` будет использовать сетевой стек контейнера `tun2socks` и в результате
весь трафик будет идти через TUN-интерфейс. Дальше уже сам `tun2socks` определит что с ним делать. Если трафик пришел снаружи, с порта 8000, то он попадёт в Audiobookshelf, как и раньше, а если же сам Audiobookshelf начнет что-то
скачивать, то запрос уйдет через TUN-интерфейс, попадет в `tun2socks`, который отфорвардит его через SOCKS5-прокси
весь трафик будет идти через TUN-интерфейс. Дальше уже сам `tun2socks` определит, что с ним делать. Если трафик пришел снаружи, с порта 8000, то он попадёт в Audiobookshelf, как и раньше, а если же сам Audiobookshelf начнет что-то
скачивать, то запрос уйдет через TUN-интерфейс, попадет в `tun2socks`, который перенаправит его через SOCKS5-прокси
`ss-ru` на Outline/VPS (на внешнем хостинге).
## Запуск и проверка
Конечно, первым делом проверить в интерфейсе Docker Station, что проект "зелененький" и все контейнеры тоже
Конечно, первым делом нужно проверить в интерфейсе Docker Station, что проект "зелененький" и все контейнеры тоже
"зелененькие". Ну или выполнить:
```bash
sudo docker ps
@@ -251,76 +251,71 @@ CONTAINER ID IMAGE COMMAND CREATED
3bdb3269ed77 shadowsocks/shadowsocks-libev "ss-local -s 90.156.…" 16s ago Up 16s ss-ru
```
Для проверки, что тоннель работает и подкасты скачиваются выполним в терминале на Synology:
Для проверки того, что тоннель работает и подкасты скачиваются, выполним в терминале на 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`. Так что придется запустить такой
Audiobookshelf работает на Node.js, а внутри контейнера нет ни `curl`, ни `wget`. Так что придется запустить такой
заковыристый js-скрипт для проверки скачивания файла.
В результате должен получим `test.mp3` в папке `/podcasts/` (а она у нас снаружи контейнера
В результате мы получим `test.mp3` в папке `/podcasts/` (а она у нас снаружи контейнера
в `/volume1/music/_podcasts`). Это выпуск №1074 подкаста "Аэростат" с альбомом Аквариума "Странные Новости с
Далёкой Звезды" (2026 год, очень рекомендую послушать). Этот подкаст размещен на DigitalOcean, так что если
Далёкой Звезды" (2024 год, очень рекомендую послушать). Этот подкаст размещен на DigitalOcean, так что если
он скачался, значит туннель работает и обход блокировок провайдера сработал.
Проверить что метаданные из iTunes тоже подгружаются, можно проверить только настроив автообновление подкастов
в интерфейсе Audiobookshelf (например через ять минут). К получение метаданных происходит "под капотом"
и иначе никак не диагностируется.
Проверить, что метаданные из iTunes тоже подгружаются, можно, только настроив автообновление подкастов
в интерфейсе Audiobookshelf (например, через пять минут). Но получение метаданных происходит "под капотом",
и иначе это никак не диагностировать.
Должно все работать. Но если не работает, то "курите логи" контейнера `audiobookshelf`:
Должно всё работать. Но если не работает, то "курите логи" контейнера `audiobookshelf`:
```bash
sudo docker logs audiobookshelf --tail 100
```
...и Google и ИИ-ассистенты в помощь. Node.js -- это не самая простая среда для отладки.
...и Google и ИИ-ассистенты в помощь. Node.js это не самая простая среда для отладки.
## P.S. Важное замечание о защите Audiobookshelf от SSRF
Во время настройки, в логах, можно столкнуться с ошибкой вида: `Call to 172.25.0.3 is blocked` или `invalid feed payload`.
Это не ошибка сети и не проблема RSS. Это встроенная защита 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`
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 адрес, в который резолвится домен, и блокирует некоторые диапазоны:
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 сети.
То есть любые внутренние адреса, включая адреса Docker-сети.
Когда использовалась схема `Audiobookshelf → HTTP Proxy → Shadowsocks` то HTTP proxy находился в Docker сети. Например:
`privoxy:8118` или `redsocks:12345`
Когда использовалась схема `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`.
После 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`), при котором приложение не знает о прокси вообще.
- иногда разрешает (резолвит) DNS на стороне прокси
- иногда разрешает (резолвит) DNS локально
В результате: часть запросов проходит, часть падает, RSS может читаться, но mp3 не скачиваются и так далее.
Опытным путем я определил, что наиболее надёжный способ — сетевой туннель (`tun2socks`), при котором приложение не знает о проксировании вообще.