Перейти к содержанию

Прокси в контейнере, который не станет общим: разбор docker-compose для SOCKS5

Опубликовано
  • Админы
SOCKS5 в контейнере

Между командой, которая поднимает SOCKS5 в контейнере, и конфигурацией, которой через сутки не пользуется посторонний, разница примерно в двадцать строк. Каждая из них что-то значит, и почти каждую можно написать неправильно так, что всё будет работать — до момента, когда начнёт работать слишком хорошо для чужих.

Открытый прокси на стандартном порту находят массовые сканеры. Не «теоретически могут найти» — находят, и порядок величины здесь часы, а не недели. После чего ваш IP начинает отвечать за спам, перебор паролей и всё, за что приходят abuse-жалобы хостеру.

Ниже — готовый docker-compose.yml, разобранный построчно: что делает каждый блок и почему он там стоит. Плюс четыре ловушки, от порта, случайно открытого в интернет, до потери SSH-доступа к собственной машине.


Чем поднимать: три кандидата

ОбразЧто внутриАутентификацияОсобенности
serjs/go-socks5-proxySOCKS5-сервер на GoREQUIRE_AUTH (по умолчанию true), PROXY_USER / PROXY_PASSWORDесть ALLOWED_IPS и ALLOWED_DEST_FQDN — фильтр по источнику и по регулярке на домен назначения
ghcr.io/tarampampam/3proxy:23proxyPROXY_LOGIN / PROXY_PASSWORDSOCKS на 1080 и HTTP-прокси на 3128 одновременно, MAX_CONNECTIONS (512 по умолчанию), свои резолверы
свой образ с Dantesockdсистемные пользователи, PAMмаксимум контроля, но конфиг придётся монтировать томом и собирать образ самому

ℹ️ Справка

Для типичной задачи «дать одному-двум приложениям выход через этот сервер» хватает первого. Второй интереснее, если нужен ещё и HTTP-прокси на том же контейнере: актуальная версия образа — v2.1.0 от 16 июня 2026, порты по умолчанию 3128/tcp (HTTP) и 1080/tcp (SOCKS), резолверы 1.0.0.1 и 8.8.4.4 задаются переменными PRIMARY_RESOLVER и SECONDARY_RESOLVER.


Compose-файл целиком

services:
  socks5:
    image: serjs/go-socks5-proxy
    container_name: socks5
    restart: unless-stopped

    environment:
      REQUIRE_AUTH: "true"
      PROXY_USER: "socksuser"
      PROXY_PASSWORD: "${SOCKS_PASSWORD:?переменная не задана}"
      PROXY_PORT: "1080"
      ALLOWED_IPS: "203.0.113.10,203.0.113.11"

    ports:
      - "127.0.0.1:1080:1080"

    healthcheck:
      test: ["CMD-SHELL", "nc -z 127.0.0.1 1080 || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    read_only: true
    tmpfs:
      - /tmp

    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Пароль лежит в .env рядом с compose-файлом, а не в самом compose:

echo "SOCKS_PASSWORD=$(openssl rand -base64 24)" > .env
chmod 600 .env

Конструкция ${SOCKS_PASSWORD:?...} — не украшение: без неё пустая переменная молча превратится в пустой пароль, и контейнер поднимется. С ней docker compose up откажется стартовать и напишет причину.

Что делает каждый блок

restart: unless-stopped. Из четырёх значений политики перезапуска (no, always, on-failure, unless-stopped) для сервиса, который должен пережить перезагрузку сервера, но остаться выключенным, если вы его выключили руками, подходит именно это. always поднимет контейнер даже после вашего осознанного docker stop — при следующем старте демона.

environment. ALLOWED_IPS — список источников через запятую. Это фильтр внутри самого прокси; он не отменяет файрвол, но закрывает случай «порт по ошибке открыт наружу, а логин угадали».

ports: "127.0.0.1:1080:1080". Самая важная строка файла. Разбор — ниже, в ловушке №1.

healthcheck. Набор полей compose: test, interval, timeout, retries, start_period и start_interval. Проверка nc -z подтверждает, что порт принимает соединения, — не то же самое, что «прокси работает», но ловит самый частый отказ: процесс упал, контейнер жив. start_period даёт контейнеру время подняться, не набирая неудачных попыток.

security_opt, cap_drop, read_only. SOCKS-прокси не нужны ни привилегии, ни право писать в свою файловую систему. Три строки — и последствия гипотетической дыры в самом прокси резко сужаются.

logging. Без ограничения размера json-file растёт до заполнения диска. Прокси — как раз тот сервис, который пишет строку на каждое соединение.


Ловушка 1. -p 1080:1080 открывает порт в интернет

В документации Docker это сказано открытым текстом: «Publishing container ports is insecure by default. Meaning, when you publish a container's ports it becomes available not only to the Docker host, but to the outside world as well».

То есть ports: - "1080:1080" — это не «пробросить порт внутрь хоста». Это «слушать 0.0.0.0:1080». Открытый SOCKS5 на стандартном порту находят сканеры, и это вопрос часов, а не недель.

Правильная форма — с явным адресом:

ports:
  - "127.0.0.1:1080:1080"     # только сам хост
  # - "10.8.0.1:1080:1080"    # только через VPN-интерфейс

Документация: «If you include the localhost IP address (127.0.0.1, or ::1) with the publish flag, only the Docker host can access the published container port».

⚠️ Осторожно

Оговорка из той же документации: в версиях Docker до 28.0.0 хосты в том же сегменте локальной сети могли достучаться до портов, опубликованных на localhost, вопреки ожидаемому поведению. Проверьте docker version — если сервер на старой ветке, не полагайтесь на 127.0.0.1 как на единственную защиту.

Как тогда пользоваться прокси с ноутбука, если он слушает только петлю на сервере? Через SSH:

# на клиенте: локальный 1080 → 127.0.0.1:1080 на сервере
ssh -L 127.0.0.1:1080:127.0.0.1:1080 -N user@vps.example.com

Получается два туннеля вместо одного, зато наружу не торчит ничего. Альтернатива — повесить публикацию на адрес WireGuard-интерфейса и ходить через VPN.


Ловушка 2. UFW не закрывает контейнерные порты

Классика: администратор пишет ufw deny 1080/tcp, проверяет ufw status, видит DENY — и порт всё равно отвечает снаружи.

Куда попадает пакет к опубликованному порту
Схема на основе docs.docker.com/engine/network/packet-filtering-firewalls

Официальное объяснение: «When you publish a container's ports using Docker, traffic to and from that container gets diverted before it goes through the ufw firewall settings». Причина техническая: «Docker routes container traffic in the nat table, which means that packets are diverted before it reaches the INPUT and OUTPUT chains that ufw uses». Итог — «Packets are routed before the firewall rules can be applied, effectively ignoring your firewall configuration».

⛔ Так делать нельзя

Не считайте ufw status доказательством того, что порт закрыт. Единственная надёжная проверка — попытка подключиться с другой машины:

nc -vz vps.example.com 1080

Connection refused или таймаут — хорошо. succeeded — плохо, независимо от того, что показывает ufw.

Практических выходов два, и первый предпочтительнее:

  1. Не публиковать порт наружу вообще127.0.0.1:1080:1080, как выше. Тогда вопрос файрвола для этого сервиса просто не возникает.
  2. Если публиковать всё же надо — фильтровать в цепочке DOCKER-USER, которая обрабатывается до правил Docker, либо использовать известный набор правил ufw-docker. Тема заслуживает отдельного разбора.

Ловушка 3. DNS резолвится не там, где вы думаете

Контейнер получает резолверы от Docker (по умолчанию — встроенный 127.0.0.11, который проксирует к резолверам хоста). Прокси-сервер внутри контейнера будет резолвить имена через них.

Дальше начинается путаница из двух слоёв:

  • Слой клиента. Если клиент ходит по схеме socks5://, он резолвит имя сам и отдаёт прокси уже IP. Всё, что вы настроили внутри контейнера, к делу не относится. Нужна схема socks5h:// (или флаг --socks5-hostname у curl) — тогда имя резолвит прокси.
  • Слой контейнера. Уже после этого имеет значение, какими резолверами пользуется контейнер. Если сервер стоит там, где локальный DNS отвечает подменёнными адресами, прокси добросовестно вернёт вам ту же подмену.

Задать резолверы явно:

services:
  socks5:
    dns:
      - 1.1.1.1
      - 9.9.9.9

Проверка, что имя реально резолвит сервер, а не клиент, — на самом сервере:

# в одном терминале
sudo tcpdump -i any -n port 53
# в другом, с клиента
curl -x socks5h://socksuser:PASS@127.0.0.1:1080 https://ifconfig.co

Запрос на 53-й порт появился — значит, резолвит прокси. Не появился — вы забыли букву h в схеме.


Ловушка 4. Как не отрезать себе доступ к серверу

Три самых частых способа остаться снаружи собственной машины:

Правило файрвола раньше, чем проверка. ufw deny incoming без предварительного ufw allow OpenSSH — и всё. Порядок всегда такой: сначала разрешить SSH, потом закрывать остальное, и только потом ufw enable.

network_mode: host «чтобы проще». Контейнер начинает слушать на всех интерфейсах хоста, ports перестаёт работать вовсе, изоляция исчезает. Для SOCKS5 в этом нет никакой нужды: обычной bridge-сети достаточно. Про режимы сети в Docker стоит почитать отдельно.

Проверка правил без страховки. Перед экспериментами с файрволом на удалённом сервере полезно поставить отложенный откат:

# через 10 минут правила откатятся сами, если вы не отменили таймер
sudo bash -c 'echo "ufw --force reset && ufw allow OpenSSH && ufw --force enable" | at now + 10 minutes'

Убедились, что доступ жив, — снимаете задание через atrm. Не убедились — сервер сам себя починит.

✅ Как правильно

Чек-лист перед тем, как оставить прокси работать:

  • docker compose config — посмотреть, во что развернулись переменные, и убедиться, что пароль не пустой
  • ss -tlnp | grep 1080 на сервере — слушает 127.0.0.1, а не 0.0.0.0
  • nc -vz <публичный_ip> 1080 с другой машины — соединения нет
  • curl -x socks5h://user:pass@127.0.0.1:1080 https://ifconfig.co — отдаёт IP сервера
  • curl -x socks5h://127.0.0.1:1080 https://ifconfig.co без пароля — отказ
  • docker compose logs --tail=50 socks5 — в логах видны ваши подключения и ничьи больше

Про безопасность — коротко и по делу

🔒 Безопасность

Открытый прокси — проблема не того, кто им пользуется, а того, кому принадлежит IP. Через ваш контейнер наружу пойдут чужие соединения с вашего адреса: спам, перебор паролей к чужим панелям, сканирование, скачивание того, за что придёт abuse-жалоба. Отвечать перед хостером будете вы, и «я не знал, что порт открыт» — не аргумент; в худшем сценарии машину выключат без предупреждения.

Что обязательно, а не «желательно»:

  1. Аутентификация всегда. REQUIRE_AUTH: "true" и непустой пароль. Никаких «на пару часов для теста» — тестовые конфигурации живут годами.
  2. Порт не в интернет. Публикация на 127.0.0.1 или на адрес VPN-интерфейса, а доступ снаружи — через SSH или WireGuard.
  3. Ограничение по источнику. ALLOWED_IPS в переменных плюс правило в DOCKER-USER, если порт всё-таки опубликован.
  4. Логи, которые кто-то читает. Ограниченные по размеру, но не выключённые. Чужой IP в логах — единственный сигнал, что прокси нашли.
  5. Пароль не в git. .env в .gitignore, права 600.

Источники

💬 Вопрос к сообществу

Открытый вопрос к тем, кто держит прокси в контейнерах: вы ограничиваете исходящие соединения самого контейнера? Прокси по определению ходит куда попросят, и правило «наружу можно только 80/443» ломает половину сценариев — но и превращает контейнер в куда менее интересную цель. Кто нашёл рабочий компромисс?

Featured Replies

No posts to show

Присоединяйтесь к обсуждению

Вы можете написать сейчас и зарегистрироваться позже. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.
Примечание: Ваш пост будет проверен модератором, прежде чем станет видимым.

Гость
К сожалению, ваш контент содержит запрещённые слова. Пожалуйста, отредактируйте контент, чтобы удалить выделенные ниже слова.
Ответить в этой теме...

Последние посетители 0

  • Ни одного зарегистрированного пользователя не просматривает данную страницу
Яндекс.Метрика

Account

Navigation

Поиск

Поиск

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.