Загрузка данных


<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SOCKS5 на своём VPS: 3proxy в Docker</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
  --bg: #F4F6F7;
  --panel: #FFFFFF;
  --ink: #191D21;
  --muted: #5B646C;
  --rule: #DCE1E4;
  --allow: #1D6F42;
  --deny: #A33A22;
  --link: #2A4E7C;
  --term-bg: #11161B;
  --term-ink: #D8E0E7;
  --term-dim: #7E8C99;
  --code-bg: #EAEEF0;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14181B;
    --panel: #1B2025;
    --ink: #E3E8EB;
    --muted: #97A1A9;
    --rule: #2C333A;
    --allow: #5FBE86;
    --deny: #E08a6d;
    --link: #8FB4E3;
    --term-bg: #0D1115;
    --term-ink: #D8E0E7;
    --term-dim: #76838F;
    --code-bg: #232A30;
  }
}
:root[data-theme="dark"] {
  --bg: #14181B;
  --panel: #1B2025;
  --ink: #E3E8EB;
  --muted: #97A1A9;
  --rule: #2C333A;
  --allow: #5FBE86;
  --deny: #E08a6d;
  --link: #8FB4E3;
  --term-bg: #0D1115;
  --term-ink: #D8E0E7;
  --term-dim: #76838F;
  --code-bg: #232A30;
}

* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  font-size: 16px;
  line-height: 1.6;
}
.wrap {
  max-width: 44rem;
  margin: 0 auto;
  padding: 2.5rem 1.25rem 5rem;
}

/* ---------- hero ---------- */
.hero { padding-bottom: 2rem; border-bottom: 1px solid var(--rule); margin-bottom: 2.5rem; }
h1 {
  font-size: clamp(1.8rem, 6vw, 2.6rem);
  line-height: 1.15;
  font-weight: 600;
  letter-spacing: -0.02em;
  margin: 0 0 0.6rem;
}
.lede { color: var(--muted); margin: 0 0 1.6rem; max-width: 34rem; }

.term {
  background: var(--term-bg);
  color: var(--term-ink);
  border-radius: 6px;
  padding: 1rem 1.1rem;
  font-family: "IBM Plex Mono", ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.84rem;
  line-height: 1.7;
  overflow-x: auto;
}
.term .p { color: var(--term-dim); }
.term .out { color: #8FD3AC; }

/* ---------- structure ---------- */
h2 {
  font-size: 1.15rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 3rem 0 0.4rem;
  display: flex;
  gap: 0.7rem;
  align-items: baseline;
}
h2 .n {
  font-family: "IBM Plex Mono", monospace;
  font-size: 0.8rem;
  font-weight: 500;
  color: var(--muted);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0.05rem 0.4rem;
  flex: none;
}
h3 { font-size: 0.95rem; font-weight: 600; margin: 1.8rem 0 0.4rem; }
p { margin: 0.7rem 0; }
ul, ol { margin: 0.7rem 0; padding-left: 1.2rem; }
li { margin: 0.3rem 0; }
a { color: var(--link); }

code {
  font-family: "IBM Plex Mono", ui-monospace, Menlo, Consolas, monospace;
  font-size: 0.86em;
  background: var(--code-bg);
  padding: 0.1em 0.35em;
  border-radius: 3px;
}
pre {
  background: var(--term-bg);
  color: var(--term-ink);
  border-radius: 6px;
  padding: 0.95rem 1.05rem;
  overflow-x: auto;
  font-size: 0.82rem;
  line-height: 1.65;
  margin: 0.9rem 0;
}
pre code { background: none; padding: 0; font-size: 1em; color: inherit; }
pre .c { color: var(--term-dim); }

.note {
  border-left: 3px solid var(--rule);
  padding: 0.1rem 0 0.1rem 0.9rem;
  margin: 1.1rem 0;
  color: var(--muted);
  font-size: 0.93rem;
}
.warn { border-left-color: var(--deny); color: var(--ink); }
.warn strong { color: var(--deny); font-weight: 600; }

table { border-collapse: collapse; width: 100%; font-size: 0.9rem; margin: 1rem 0; }
th, td { text-align: left; padding: 0.55rem 0.6rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
th { font-weight: 600; color: var(--muted); font-size: 0.82rem; }
.scroll { overflow-x: auto; }

.vars { background: var(--panel); border: 1px solid var(--rule); border-radius: 6px; padding: 0.4rem 1rem; margin: 1.2rem 0; }
.vars dl { margin: 0.6rem 0; display: grid; grid-template-columns: max-content 1fr; gap: 0.35rem 1rem; font-size: 0.92rem; }
.vars dt { font-family: "IBM Plex Mono", monospace; font-size: 0.85rem; }
.vars dd { margin: 0; color: var(--muted); }

footer { margin-top: 4rem; padding-top: 1.5rem; border-top: 1px solid var(--rule); color: var(--muted); font-size: 0.88rem; }
@media (max-width: 480px) {
  .vars dl { grid-template-columns: 1fr; gap: 0.1rem 0; }
  .vars dd { margin-bottom: 0.5rem; }
}
</style>
</head>
<body>
<div class="wrap">

<div class="hero">
  <h1>SOCKS5 на своём VPS</h1>
  <p class="lede">3proxy в Docker с авторизацией по логину и паролю, доступом только с двух ваших адресов и автозапуском после перезагрузки. Вот момент, ради которого всё делается:</p>
  <div class="term">
<span class="p">$</span> curl -x socks5h://127.0.0.1:41904 --proxy-user 'user:pass' https://ifconfig.me
<span class="out">203.0.113.17</span>
<span class="p">$</span> nmap -p 41904 203.0.113.17   <span class="p"># с чужого адреса</span>
<span class="out">41904/tcp filtered unknown</span>
  </div>
</div>

<p>Инструкция воспроизводит то, что делает приложение Amnezia, но с двумя исправлениями: прокси получает рабочий DNS, а порт с самого начала закрыт для всех, кроме вас. Все команды выполняются на сервере от root.</p>

<h2><span class="n">1</span>Собрать исходные данные</h2>

<p>Прежде чем что-то ставить, выпишите четыре значения. Дальше они встречаются в каждом файле, и путаница в них — источник почти всех проблем.</p>

<div class="vars">
<dl>
  <dt>PORT</dt><dd>Порт прокси. Возьмите произвольный из диапазона 20000–60000, не 1080 — по нему сканеры ищут прокси в первую очередь. Дальше в примерах <code>41904</code>.</dd>
  <dt>IFACE</dt><dd>Имя внешнего сетевого интерфейса сервера.</dd>
  <dt>IP1, IP2</dt><dd>Два ваших статических адреса, с которых разрешено подключаться.</dd>
</dl>
</div>

<p>Интерфейс — так:</p>
<pre><code>ip route get 1.1.1.1</code></pre>
<p>В выводе ищите <code>dev eth0</code> — это и есть имя (может быть <code>ens3</code>, <code>enp1s0</code>).</p>

<p>Свои адреса узнайте с каждой из двух машин:</p>
<pre><code>curl ifconfig.me</code></pre>

<p>Docker должен быть установлен. Проверка:</p>
<pre><code>docker --version &amp;&amp; docker compose version</code></pre>
<p>Если команд нет:</p>
<pre><code>curl -fsSL https://get.docker.com | sh</code></pre>

<div class="note">Держите открытой вторую SSH-сессию к серверу всё время настройки файрвола. Если ошибётесь в правиле и отрежете себе доступ, через неё откатите.</div>

<h2><span class="n">2</span>Создать каталоги</h2>

<pre><code>mkdir -p /opt/socks5/logs
cd /opt/socks5</code></pre>

<p>3proxy внутри контейнера работает не от root: он делает chroot и сбрасывает права на UID 65535. Каталог логов должен принадлежать этому UID, иначе процесс не сможет писать:</p>

<pre><code>chown -R 65535:65535 /opt/socks5/logs</code></pre>

<h2><span class="n">3</span>Придумать логин и пароль</h2>

<pre><code>openssl rand -hex 4    <span class="c"># логин</span>
openssl rand -base64 18 | tr -d '/+=' <span class="c"># пароль</span></code></pre>

<p>Символы <code>/ + =</code> вырезаются намеренно. Пароль часто приходится вставлять в URL вида <code>socks5h://логин:пароль@хост</code>, и спецсимволы там ломают разбор адреса — клиент выдаёт непонятную ошибку, а вы думаете, что дело в сервере.</p>

<p>Запишите оба значения, они понадобятся в следующем шаге и во всех клиентах.</p>

<h2><span class="n">4</span>Написать конфиг 3proxy</h2>

<p>Создайте файл <code>/opt/socks5/3proxy.cfg</code>. Подставьте свои порт, логин и пароль:</p>

<pre><code>nserver 1.1.1.1
nserver 8.8.8.8
nscache 65536

timeouts 1 5 30 60 180 1800 15 60
maxconn 200

users ВАШЛОГИН:CL:ВАШПАРОЛЬ
auth strong
allow ВАШЛОГИН

log /logs/3proxy.log D
rotate 7
logformat "- %t %U %C:%c %R:%r %O/%I %n %E"

socks -p41904</code></pre>

<p>Что делает каждый блок:</p>

<div class="scroll">
<table>
<tr><th>Строка</th><th>Зачем</th></tr>
<tr><td><code>nserver</code></td><td>Адреса DNS-серверов. Критично: после chroot у процесса нет доступа к <code>/etc/resolv.conf</code>, и без этих строк прокси не может резолвить имена. Симптом — соединение устанавливается, но любой запрос отваливается с кодом 2.</td></tr>
<tr><td><code>nscache</code></td><td>Кэш DNS-ответов, заметно ускоряет работу.</td></tr>
<tr><td><code>users</code></td><td>Учётная запись. <code>CL</code> означает, что пароль хранится открытым текстом в этом файле — поэтому файл не должен быть доступен посторонним.</td></tr>
<tr><td><code>auth strong</code></td><td>Пускать только с верным логином и паролем. Без этой строки прокси открыт для всех.</td></tr>
<tr><td><code>allow</code></td><td>Явное разрешение для вашего пользователя. Как только появляется хотя бы одно правило <code>allow</code>, всё, что под него не подошло, автоматически запрещается.</td></tr>
<tr><td><code>log ... D</code></td><td>Лог с ежедневной ротацией, <code>rotate 7</code> хранит неделю. Путь <code>/logs</code> указан относительно chroot — на файловой системе контейнера это <code>/usr/local/3proxy/logs</code>.</td></tr>
<tr><td><code>socks -p</code></td><td>Запуск SOCKS-сервиса на выбранном порту.</td></tr>
</table>
</div>

<p>Закройте файл от чужих глаз:</p>
<pre><code>chmod 600 /opt/socks5/3proxy.cfg</code></pre>

<h2><span class="n">5</span>Описать контейнер</h2>

<p>Файл <code>/opt/socks5/docker-compose.yml</code>:</p>

<pre><code>services:
  socks5:
    image: 3proxy/3proxy:latest
    container_name: socks5
    restart: unless-stopped
    ports:
      - "41904:41904"
    volumes:
      - ./3proxy.cfg:/usr/local/3proxy/conf/3proxy.cfg:ro
      - ./logs:/usr/local/3proxy/logs
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"</code></pre>

<p>Три вещи, которые здесь важны:</p>

<ul>
<li><strong>Порты одинаковые с обеих сторон.</strong> 3proxy слушает тот порт, который задан в конфиге, поэтому внутренний и внешний должны совпадать. Это упрощает жизнь на шаге с файрволом.</li>
<li><strong>Путь монтирования конфига.</strong> Готовый образ запускает <code>/etc/3proxy/3proxy.cfg</code>, а тот содержит всего две строки: chroot в <code>/usr/local/3proxy</code> и подключение <code>/conf/3proxy.cfg</code>. Внутри chroot этот путь превращается в <code>/usr/local/3proxy/conf/3proxy.cfg</code> — туда мы и подкладываем свой файл.</li>
<li><strong>Порт открыт наружу.</strong> Так и задумано: доступ ограничивается на следующем шаге, файрволом. Пока шаг 7 не выполнен, прокси доступен всему интернету — не оставляйте его в таком виде надолго.</li>
</ul>

<div class="note">Если хотите убедиться, что в образе действительно те две строки: <code>docker create --name tmp 3proxy/3proxy:latest</code>, затем <code>docker cp tmp:/etc/3proxy/3proxy.cfg -</code> и <code>docker rm tmp</code>. Заходить внутрь через <code>docker exec</code> бесполезно — в образе нет оболочки.</div>

<h2><span class="n">6</span>Запустить и убедиться, что живо</h2>

<pre><code>cd /opt/socks5
docker compose up -d
docker compose ps</code></pre>

<p>Статус должен быть <code>running</code>. Если контейнер в цикле перезапусков, смотрите причину:</p>

<pre><code>docker logs socks5</code></pre>

<p>Проверьте, что порт действительно слушается:</p>

<pre><code>ss -tlnp | grep 41904</code></pre>

<h2><span class="n">7</span>Закрыть порт для всех, кроме двух адресов</h2>

<p>Здесь главная ловушка всей затеи: <strong>ufw и firewalld на порты Docker не действуют</strong>. Docker добавляет свои правила в обход цепочки, с которой они работают. <code>ufw status</code> будет показывать «deny», а порт останется открытым. Правила нужно писать в цепочку <code>DOCKER-USER</code> — она создана специально для этого и проверяется раньше правил самого Docker.</p>

<p>Вторая тонкость — порядок. Нельзя написать два правила «запретить всё, кроме IP1» и «запретить всё, кроме IP2»: первое отрежет IP2, второе отрежет IP1, и не пустит никого. Правила читаются сверху вниз, и решает первое подошедшее. Поэтому логика такая: сначала явно пропустить свои адреса, последним — запретить остальное.</p>

<pre><code>iptables -I DOCKER-USER -i eth0 -p tcp --dport 41904 -j DROP
iptables -I DOCKER-USER -i eth0 -p tcp --dport 41904 -s IP2 -j RETURN
iptables -I DOCKER-USER -i eth0 -p tcp --dport 41904 -s IP1 -j RETURN</code></pre>

<p>Команды идут именно в этом порядке, потому что <code>-I</code> вставляет правило <em>в начало</em> цепочки: каждая следующая встаёт выше предыдущей, и в итоге получается IP1 → IP2 → DROP.</p>

<p><code>RETURN</code>, а не <code>ACCEPT</code>: пакет возвращается в обычную обработку Docker, вместо того чтобы проскочить мимо остальных проверок.</p>

<p>Проверьте, что порядок получился верный:</p>

<pre><code>iptables -L DOCKER-USER -n -v --line-numbers</code></pre>

<p>Сверху должны идти два <code>RETURN</code> с вашими адресами, под ними <code>DROP</code>. Если порядок другой — <code>iptables -F DOCKER-USER</code> и повторите три команды. В колонках pkts/bytes со временем появятся ненулевые значения: это признак, что правила действительно работают, а не просто висят.</p>

<h3>IPv6</h3>
<p>Если у сервера есть адрес v6, а ваши два адреса — v4, дверь с другой стороны надо закрыть целиком:</p>

<pre><code>ip -6 addr show eth0          <span class="c"># есть ли v6 вообще</span>
ip6tables -I DOCKER-USER -i eth0 -p tcp --dport 41904 -j DROP</code></pre>

<h2><span class="n">8</span>Сделать правила постоянными</h2>

<p>Цепочка <code>DOCKER-USER</code> очищается при каждом перезапуске docker-демона — в том числе при автоматическом обновлении пакета. Поэтому <code>iptables-persistent</code> тут не годится: он восстанавливается раньше Docker, и его правила стираются.</p>

<p>Файл <code>/usr/local/bin/proxy-fw.sh</code>:</p>

<pre><code>#!/bin/sh
IFACE=eth0
PORT=41904
IP1=ваш.первый.ip
IP2=ваш.второй.ip

<span class="c"># дождаться, пока Docker создаст цепочку</span>
i=0
while [ $i -lt 30 ]; do
  iptables -L DOCKER-USER -n >/dev/null 2>&amp;1 &amp;&amp; break
  i=$((i+1)); sleep 1
done

<span class="c"># снять прошлые копии, чтобы не плодить дубли</span>
iptables -D DOCKER-USER -i $IFACE -p tcp --dport $PORT -j DROP 2>/dev/null
iptables -D DOCKER-USER -i $IFACE -p tcp --dport $PORT -s $IP2 -j RETURN 2>/dev/null
iptables -D DOCKER-USER -i $IFACE -p tcp --dport $PORT -s $IP1 -j RETURN 2>/dev/null

iptables -I DOCKER-USER -i $IFACE -p tcp --dport $PORT -j DROP
iptables -I DOCKER-USER -i $IFACE -p tcp --dport $PORT -s $IP2 -j RETURN
iptables -I DOCKER-USER -i $IFACE -p tcp --dport $PORT -s $IP1 -j RETURN

<span class="c"># раскомментируйте, если у сервера есть IPv6</span>
<span class="c"># ip6tables -D DOCKER-USER -i $IFACE -p tcp --dport $PORT -j DROP 2>/dev/null</span>
<span class="c"># ip6tables -I DOCKER-USER -i $IFACE -p tcp --dport $PORT -j DROP</span></code></pre>

<pre><code>chmod +x /usr/local/bin/proxy-fw.sh</code></pre>

<p>Юнит <code>/etc/systemd/system/proxy-fw.service</code>:</p>

<pre><code>[Unit]
After=docker.service
Requires=docker.service
PartOf=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/proxy-fw.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target</code></pre>

<p><code>PartOf=docker.service</code> — ключевая строка. Без неё юнит отработает один раз при загрузке и больше не запустится, а при перезапуске Docker правила молча исчезнут.</p>

<pre><code>systemctl daemon-reload
systemctl enable --now proxy-fw
systemctl is-enabled proxy-fw    <span class="c"># должно вывести enabled</span></code></pre>

<h2><span class="n">9</span>Проверить — три теста</h2>

<p><strong>С самого сервера</strong> — жив ли прокси и работает ли у него DNS:</p>

<pre><code>curl -v -x socks5h://127.0.0.1:41904 --proxy-user 'ЛОГИН:ПАРОЛЬ' https://ifconfig.me</code></pre>

<p>Должен вернуться публичный IP сервера. Схема <code>socks5h</code> означает, что имя резолвит прокси — именно это и проверяется.</p>

<p><strong>С каждой из двух разрешённых машин</strong> — пускает ли вас:</p>

<pre><code>curl -x socks5h://IP_СЕРВЕРА:41904 --proxy-user 'ЛОГИН:ПАРОЛЬ' https://ifconfig.me</code></pre>

<p><strong>С постороннего адреса</strong> — не пускает ли чужих. Проще всего с телефона по мобильному интернету, отключив Wi-Fi:</p>

<pre><code>nmap -p 41904 IP_СЕРВЕРА</code></pre>

<p>Ожидаемый ответ — <code>filtered</code>. Если видите <code>open</code>, значит правила не применились: вернитесь к шагу 7 и проверьте имя интерфейса и номер порта. Этот тест обязателен — без него вы не знаете, закрыт порт или нет.</p>

<h2><span class="n">10</span>Подключить клиенты</h2>

<p>Везде указывайте хост (IP сервера), порт, логин и пароль. Главное правило: если в настройке есть выбор «резолвить DNS через прокси» — включайте его. В командной строке это схема <code>socks5h</code>, а не <code>socks5</code>; иначе имена сайтов резолвит ваша машина, и провайдер видит, куда вы ходите, даже если сам трафик идёт через сервер.</p>

<ul>
<li><strong>Telegram:</strong> Настройки → Данные и память → Прокси → Добавить прокси → SOCKS5.</li>
<li><strong>Firefox:</strong> Настройки → Сеть → Настроить → SOCKS5, и обязательно галочка «Проксировать DNS при использовании SOCKS 5».</li>
<li><strong>Chrome:</strong> своих настроек прокси нет, берёт системные. Удобнее расширение вроде Proxy SwitchyOmega — заодно можно пускать через прокси только нужные сайты.</li>
<li><strong>Системно на Linux:</strong> <code>export ALL_PROXY=socks5h://ЛОГИН:ПАРОЛЬ@IP:41904</code></li>
</ul>

<h2><span class="n">11</span>Обслуживание</h2>

<h3>Кто подключался</h3>
<pre><code>tail -f /opt/socks5/logs/3proxy.log</code></pre>
<p>В логе видны IP клиентов. Чужих адресов там быть не должно — если появились, значит файрвол не работает.</p>

<h3>Сменить пароль</h3>
<p>Отредактировать строку <code>users</code> в <code>3proxy.cfg</code>, затем <code>docker compose restart</code>.</p>

<h3>Обновить образ</h3>
<pre><code>cd /opt/socks5 &amp;&amp; docker compose pull &amp;&amp; docker compose up -d</code></pre>

<h3>Удалить всё</h3>
<pre><code>cd /opt/socks5 &amp;&amp; docker compose down
systemctl disable --now proxy-fw
rm /etc/systemd/system/proxy-fw.service /usr/local/bin/proxy-fw.sh
iptables -F DOCKER-USER
rm -rf /opt/socks5</code></pre>

<h2><span class="n">12</span>Если что-то не работает</h2>

<div class="scroll">
<table>
<tr><th>Симптом</th><th>Причина и что делать</th></tr>
<tr><td><code>Can't complete SOCKS5 connection ... (2)</code></td><td>Соединение и авторизация прошли, а сам запрос отклонён. Почти всегда — не работает DNS: проверьте, что строки <code>nserver</code> есть в конфиге и что конфиг действительно подмонтировался. Быстрая проверка: <code>curl -x socks5://... https://1.1.1.1</code> (без <code>h</code>, адресом-цифрами) — если так работает, дело именно в резолвинге.</td></tr>
<tr><td>Ошибка авторизации, отказ сразу после подключения</td><td>Неверный логин или пароль, либо имя в <code>allow</code> не совпадает с именем в <code>users</code>. Если пароль вставляли в URL — попробуйте вынести его в <code>--proxy-user</code>, спецсимволы ломают разбор адреса.</td></tr>
<tr><td>Порт снаружи <code>open</code> вместо <code>filtered</code></td><td>Правило не совпадает с трафиком. Сверьте имя интерфейса (<code>ip route get 1.1.1.1</code>) и номер порта, посмотрите счётчики в <code>iptables -L DOCKER-USER -n -v</code>: нули означают, что правило мёртвое.</td></tr>
<tr><td>После перезагрузки правил нет</td><td><code>systemctl status proxy-fw</code>. Чаще всего забыт <code>PartOf=docker.service</code> или не сделан <code>systemctl enable</code>.</td></tr>
<tr><td>Контейнер постоянно перезапускается</td><td><code>docker logs socks5</code> — обычно опечатка в конфиге или каталог логов не принадлежит UID 65535.</td></tr>
<tr><td>Лог-файл не появляется</td><td>Попробуйте в конфиге полный путь <code>/usr/local/3proxy/logs/3proxy.log</code> вместо <code>/logs/3proxy.log</code> и перезапустите контейнер.</td></tr>
</table>
</div>

<h2><span class="n">13</span>Что эта схема не закрывает</h2>

<p>Фильтрация по IP снимает главный практический риск — что ваш сервер используют посторонние и хостер заблокирует его за рассылку спама. Но две вещи остаются:</p>

<ul>
<li><strong>Пароль передаётся открытым текстом.</strong> Так устроен сам протокол. Перехватить его может только тот, кто находится на пути трафика. Данные внутри HTTPS при этом остаются зашифрованными — прокси видит лишь имя хоста.</li>
<li><strong>Соединение легко распознаётся.</strong> Рукопожатие SOCKS5 не замаскировано, и системы анализа трафика определяют его тривиально.</li>
</ul>

<p>Если нужна не только защита от чужих, но и незаметность, прокси не надо публиковать наружу вовсе: замените в compose строку портов на <code>"127.0.0.1:41904:41904"</code> и ходите через SSH-туннель <code>ssh -L 41904:127.0.0.1:41904 -N user@сервер</code> — либо откройте порт только на адресе VPN-интерфейса, если на сервере уже поднят WireGuard или AmneziaWG.</p>

<footer>Проверено на Debian 12 и Ubuntu 24.04 с Docker Engine и 3proxy 0.9.x. В примерах порт 41904 — замените на свой.</footer>

</div>
</body>
</html