Нужна помощь с проксированием трафика из docker контейнеров

Всем привет, ситуация такая: есть VPS1, на которой прокси сервер с vless и VPS2, трафик с которой нужно проксировать через VPS1, для этого на нее установил sing-box и с помощью chatgpt создал такой скрипт:

#!/bin/bash
# /opt/singbox/update_openai_ips.sh
# Обновляет список IP для указанных доменов и ставит iptables-правила:
# — через ipset, если он установлен
# — или «чистыми» правилами, если ipset отсутствует

# Параметры
IPSET_NAME="openai"
FWMARK="233"
TABLE="sing"
DOMAINS=( "openai.com" "chatgpt.com" )

# Функция для «чистого» iptables-режима
function update_without_ipset() {
  echo "ℹ️  Обновляем правила iptables без ipset..."

  # Удаляем старые правила SING_MARK
  iptables -t mangle -F SING_MARK 2>/dev/null || iptables -t mangle -N SING_MARK

  # Удаляем старую привязку OUTPUT → SING_MARK
  iptables -t mangle -D OUTPUT -j SING_MARK 2>/dev/null
  # Добавляем новую
  iptables -t mangle -A OUTPUT -j SING_MARK

  # В SING_MARK для каждого IP добавляем правило маркировки
  for domain in "${DOMAINS[@]}"; do
    echo "  ↳ Resolving $domain..."
    for ip in $(dig +short "$domain" | grep -Eo '^[0-9.]+$'); do
      echo "      • $ip"
      # Удаляем старое (на всякий) и добавляем новое
      iptables -t mangle -D SING_MARK -d "$ip" -j MARK --set-mark $FWMARK 2>/dev/null
      iptables -t mangle -A SING_MARK -d "$ip" -j MARK --set-mark $FWMARK
    done
  done
}

# Функция для режима с ipset
function update_with_ipset() {
  echo "ℹ️  Обновляем ipset '$IPSET_NAME'..."
  ipset create $IPSET_NAME hash:ip -exist
  ipset flush $IPSET_NAME

  for domain in "${DOMAINS[@]}"; do
    echo "  ↳ Resolving $domain..."
    for ip in $(dig +short "$domain" | grep -Eo '^[0-9.]+$'); do
      echo "      • $ip"
      ipset add $IPSET_NAME $ip -exist
    done
  done

  echo "ℹ️  Обновляем правила iptables для ipset..."
  # Создать цепочку, если надо
  iptables -t mangle -F SING_MARK 2>/dev/null || iptables -t mangle -N SING_MARK
  # Привязать OUTPUT → SING_MARK
  iptables -t mangle -D OUTPUT -j SING_MARK 2>/dev/null
  iptables -t mangle -A OUTPUT -j SING_MARK
  # В цепочке SING_MARK одно правило: если dst в ipset → MARK
  iptables -t mangle -D SING_MARK -m set --match-set $IPSET_NAME dst -j MARK --set-mark $FWMARK 2>/dev/null
  iptables -t mangle -A SING_MARK -m set --match-set $IPSET_NAME dst -j MARK --set-mark $FWMARK
}

# Основной блок
if command -v ipset &>/dev/null; then
  update_with_ipset
else
  echo "⚠️  ipset не найден — переключаемся на чистый iptables"
  update_without_ipset
fi

# Политика маршрутизации (выполнить один раз; последующие – будут игнорироваться):
# echo "100 $TABLE" >> /etc/iproute2/rt_tables
ip rule add fwmark $FWMARK table $TABLE 2>/dev/null || true
ip route add default dev tun0 table $TABLE 2>/dev/null || true

echo "✅ Обновление завершено. Домены: ${DOMAINS[*]}"

Далее проверяю: curl https://openai.com вне докера. Проксирование присутствует, делаю то же самое из docker контейнера и проксирования нет. Что нужно сделать, что б ко всем контейнерам (к подсети докера?) применялись те же правила? Спасибо.


Ответы (1 шт):

Автор решения: Pak Uula

Если вы продолжите спрашивать у chatgpt, то он возможно вам расскажет, что трафик от контейнеров не идёт через OUTPUT, так как докер создает виртуальный интерфейс, и к трафику контейнеров применяются правила PREROUTING и FORWARD.

Поэтому вам нужно добавить правила меток в цепочку PREROUTING.

Как понять, что пакет идёт из контейнера? Это самый хитрый момент во всей этой истории. Опыт показывает, что если докеру явным образом не указать диапазон адресов для подсетки, то для каждого контейнера он будет создавать новую подсетку от 172.18.0.0/16 до 172.31.0.0/16. Все эти подсетки покрываются маской 172.16.0.0/12, включая подсетку docker0 172.17.0.0/24.

добавьте переменную

DOCKER_SUBNET=172.16.0.0/12

Если у вас какие-то специфические настройки, то пара идей в приведены в конце этого поста.

Добавьте в цикл update_without_ipset

  iptables -t mangle -D PREROUTING -s $DOCKER_SUBNET -d "$ip" -j MARK --set-mark $FWMARK 2>/dev/null
  iptables -t mangle -A PREROUTING -s $DOCKER_SUBNET -d "$ip" -j MARK --set-mark $FWMARK

то есть для всех пакетов из подсети докера в адрес chatgpt применять правило -j MARK --set-mark $FWMARK

про ipset я не в курсе, но из общих соображений

  iptables -t mangle -D PREROUTING -d $DOCKER_SUBNET -m set --match-set $IPSET_NAME dst -j MARK --set-mark $FWMARK 2>/dev/null
  iptables -t mangle -A PREROUTING -d $DOCKER_SUBNET -m set --match-set $IPSET_NAME dst -j MARK --set-mark $FWMARK

НЕ ТЕСТИРОВАЛ!

Собственно, как узнать подсеть контейнера. Если вы знаете имя контейнера, то вот вам скрипт

# Имя контейнера, которое вы хотите инспектировать
CONT_NAME="ваш-контейнер" 

# Получаем ID первой сети, к которой подключен контейнер
NETWORK_NAME=$(docker inspect "$CONT_NAME" | jq -r '.[0].NetworkSettings.Networks | keys | .[0]')

# Если имя сети найдено, получаем её подсеть
if [ -n "$NETWORK_NAME" ]; then
  DOCKER_SUBNET=$(docker network inspect "$NETWORK_NAME" | jq -r '.[0].IPAM.Config[0].Subnet')
else
  echo "Ошибка: Не удалось найти сетевое имя для контейнера $CONT_NAME." >&2
  exit 1
fi

Либо вы можете руками задать параметры сети в вашем compose.yaml и задать DOCKER_SUBNET равным подсетке из compose.yaml

→ Ссылка