دسترسی به کانتینرها در شبکه Gluetun
کانتینر متصل به Gluetun فاقد رابط شبکه مستقل است. برای دسترسی، پورتها را در Gluetun تعریف کرده و تنها زیرشبکههای ضروری را در تنظیمات فایروال برای خروج از تونل باز کنید.
وقتی یک کانتینر به شبکه gluetun متصل میشود چه اتفاقی میافتد
کانتینری که network_mode: service:gluetun را تنظیم میکند، هیچ رابط شبکه مستقلی ندارد. این کانتینر به فضای نام شبکه (network namespace) متعلق به gluetun میپیوندد؛ بنابراین انتشار پورتها و قوانین فایروال دیگر ویژگیهای آن کانتینر نیستند، بلکه به ویژگیهای سرویس gluetun تبدیل میشوند. تمام پاسخهای زیر از همین یک واقعیت ناشی میشوند.
فضای نام شبکه، کپی خصوصی هسته از یک پشته شبکه است: شامل رابطهای اختصاصی، جدول مسیریابی، قوانین فایروال و سوکتهای در حال گوش دادن (listening sockets) مخصوص به خود. Docker بهصورت پیشفرض برای هر کانتینر یک فضای نام ایجاد میکند. وقتی شما network_mode: service:gluetun را مینویسید، Docker از آن مرحله صرفنظر کرده و کانتینر جدید را درون فضای نامی قرار میدهد که از قبل متعلق به gluetun است. کانتینر همچنان سیستم فایل و فایل /etc/hosts اختصاصی خود را حفظ میکند و مورد دوم در ادامه اهمیت پیدا میکند.
شما میتوانید این موضوع را مستقیماً مشاهده کنید.
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrentاین دستور container: را به همراه شناسه کانتینر gluetun چاپ میکند، در حالی که یک کانتینر معمولی bridge را چاپ خواهد کرد. این راهنما از جایی ادامه مییابد که مسیریابی ترافیک Docker از طریق VPN با gluetun متوقف شده بود: تونل کار میکند و اکنون هیچچیز نمیتواند با کانتینر ارتباط برقرار کند.
انتشار پورت را روی gluetun انجام دهید، نه روی برنامه
یک بلوک ports: روی سرویس قرار دهید که network_mode را تنظیم میکند و Docker از ایجاد container خودداری میکند:
Error response from daemon: conflicting options: port publishing and the container type network modeدلیل این امر مستقیم است. انتشار یک پورت به معنای افزودن یک قانون NAT (ترجمه آدرس شبکه) است که پورت میزبان را به فضای نام شبکه (network namespace) خودِ container هدایت میکند، و این container چنین فضایی ندارد. نگاشت (mapping) را به سرویس gluetun منتقل کنید. شماره پورت تغییر نمیکند، زیرا برنامه همچنان در حال گوش دادن روی همان پورت در فضای نام مشترک است.
services:
gluetun:
ports:
- "8080:8080/tcp" # qBittorrent web UI
qbittorrent:
network_mode: "service:gluetun"
# no ports: block hereیک بلوک expose: روی سرویس وابسته نیز به همان اندازه بیفایده است و یک بلوک networks: در آنجا یک توقف قطعی ایجاد میکند: Compose گزارش میدهد که سرویس، مقادیر متناقض network_mode و networks را اعلام کرده است و از بارگذاری فایل بهطور کامل خودداری میکند.
یک پیامد در مراحل بعدی ظاهر میشود. هر container در این فضای نام، یک فضای پورت واحد را به اشتراک میگذارد، بنابراین دو برنامهای که هر دو بهصورت پیشفرض از 8080 استفاده میکنند با هم تداخل پیدا میکنند و دومی که شروع به کار میکند با خطای address already in use مواجه میشود. یکی از آنها را در پیکربندی خودش تغییر دهید، برای مثال متغیر WEBUI_PORT در image مربوط به qBittorrent از LinuxServer، سپس شماره پورت جدید را روی gluetun منتشر کنید.
کانتینرهای پشت gluetun چگونه به یکدیگر دسترسی پیدا میکنند؟
درون فضای نام (namespace)، آنها از قبل یک رابط loopback مشترک دارند. کانتینری که پشت gluetun قرار دارد، بدون درگیر کردن شبکه Docker، به کانتینر همتای خود در 127.0.0.1:<port> دسترسی پیدا میکند.
از خارج از فضای نام، کانتینر هیچ نامی ندارد. DNS تعبیهشده در Docker، نام سرویس را به آدرس آن سرویس در یک شبکه تعریفشده توسط کاربر ترجمه میکند، و این کانتینر در هیچ شبکهای آدرسی ندارد. بنابراین یک کانتینر معمولی مانند Sonarr به کلاینت تورنت در http://qbittorrent:8080 دسترسی پیدا نمیکند. دسترسی به آن از طریق http://gluetun:8080 انجام میشود، زیرا سوکت در فضای نام gluetun و روی آدرس gluetun در حال گوش دادن است. این موضوع برای کسانی که نحوه عملکرد شبکهها و نامگذاری سرویسها در Docker Compose را میدانند و انتظار دارند قوانین نامگذاری معمول اعمال شود، تعجبآور است. این کار همچنین بدون نیاز به publish کردن هیچ پورتی روی میزبان (host) انجام میشود، زیرا هر دو کانتینر در یک شبکه Compose قرار دارند.
پیش از عیبیابی هر چیز دیگری، DNS را بررسی کنید. gluetun resolver اختصاصی خود را اجرا میکند و /etc/resolv.conf را در کانتینر خود بازنویسی میکند، اما /etc/resolv.conf فایلی است که برای هر کانتینر جداگانه تعریف میشود؛ بنابراین فایلی که gluetun نوشته است، همان فایلی نیست که برنامه شما میخواند.
docker exec qbittorrent cat /etc/resolv.confچگونه به سرویسی که روی Docker host اجرا میشود دسترسی پیدا کنم؟
از host.docker.internal استفاده کنید. این کار نیازمند دو تنظیم در دو جای مختلف است، زیرا دو بخش متفاوت دچار مشکل هستند.
ابتدا نام را مشخص کنید. /etc/hosts مختص هر container است، بنابراین ورودی extra_hosts باید در فایل پیکربندی container برنامه قرار بگیرد، نه در gluetun.
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway یک مقدار ویژه است که Docker آن را با آدرس داخلی خودِ host جایگزین میکند. در یک نصب استاندارد Docker روی Linux، این آدرس همان پل docker0 است که معمولاً 172.17.0.1 میباشد. آدرس خود را با دستور ip -4 addr show docker0 روی VPS تأیید کنید. Docker Desktop این نام را بهصورت خودکار resolve میکند، به همین دلیل است که راهنماهایی که برای لپتاپ نوشته شدهاند از خط extra_hosts صرفنظر میکنند و همان فایل در سرور با شکست مواجه میشود.
سپس مسیر (route) را اضافه کنید. افزودن نام فقط به container میگوید از چه آدرسی استفاده کند. بسته (packet) همچنان از طریق مسیر پیشفرض gluetun، یعنی همان تونل، خارج میشود و فایروال gluetun آن را مسدود میکند. نشانهٔ این وضعیت، اتصالی است که معلق میماند و سپس timeout میدهد، نه اتصالی که رد (refuse) میشود. رد شدن به این معناست که بسته رسیده و پاسخی منفی دریافت کرده است، اما timeout به این معناست که بسته هرگز به مقصد نرسیده است.
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32سپس بررسی کنید که سرویس host واقعاً روی آن آدرس در حال گوش دادن (listen) باشد. یک سرور PostgreSQL که فقط به 127.0.0.1 متصل (bind) شده باشد، از هیچ containerای (چه با تونل و چه بدون آن) قابل دسترسی نیست، زیرا 127.0.0.1 در داخل namespace، همان loopback خودِ آن namespace است. بهجای آن، سرویس را به 172.17.0.1 متصل کنید: این کار باعث میشود اتصالات از سمت containerها پذیرفته شوند و در عین حال سرویس روی رابط عمومی (public interface) قرار نگیرد. این مورد را با ss -lntp | grep 5432 روی host بررسی کنید.
تغییرات واقعی FIREWALL_OUTBOUND_SUBNETS
مستندات gluetun این متغیر را به عنوان زیرشبکههایی که با کاما از هم جدا شدهاند تعریف میکند که gluetun و کانتینرهای متصل به شبکه آن اجازه دسترسی به آنها را دارند. این تنظیم شامل تغییرات در فایروال و مسیریابی است و هر دو بخش اهمیت دارند. gluetun برای هر زیرشبکه لیستشده، یک مسیر از طریق gateway پل Docker اضافه میکند تا بستههای مربوط به آن آدرسها به جای تونل، از طریق eth0 ارسال شوند. همچنین این تنظیم فایروال را برای آن زیرشبکهها باز میکند، زیرا gluetun در حالت عادی ترافیک خروجی که مقصد آن سرور VPN نباشد را مسدود میکند.
مقدار را بدون فاصله بعد از کاما وارد کنید.
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32دو ویژگی ممکن است نادیده گرفته شوند. این یک تنظیم در سطح namespace است، بنابراین برای تمام کانتینرهای پشت gluetun اعمال میشود، نه فقط کانتینری که مد نظر شماست. همچنین این تنظیم فقط مربوط به ترافیک خروجی است و اتصالات آغازشده توسط کانتینر را کنترل میکند. اتصالاتی که به یک پورت منتشرشده (published port) میرسند، مسیر متفاوتی را طی میکنند و نیازی به تعریف در اینجا ندارند.
دسترسی به رابط کاربری وب از طریق یک peer در Tailscale
Tailscale به هر دستگاه یک آدرس در محدوده 100.64.0.0/10 اختصاص میدهد که برای NAT در سطح اپراتور رزرو شده است. برقراری ارتباط در هر دو جهت نیازمند اقدامات متفاوتی است.
دسترسی ورودی سادهتر است. انتشار 8080:8080 روی gluetun باعث میشود این پورت روی تمام آدرسهای میزبان bind شود و رابط tailscale0 میزبان نیز یکی از آنهاست؛ بنابراین یک peer با باز کردن http://<machine-name>:8080 به container دسترسی پیدا میکند. در این مسیر، gluetun نقشی ندارد، زیرا قانون NAT داکر روی میزبان و خارج از namespace قرار دارد.
برای اینکه رابط کاربری فقط از طریق tailnet قابل دسترسی باشد، پورت منتشرشده را به جای تمام آدرسها، فقط به آدرس Tailscale میزبان bind کنید.
ports:
- "100.101.102.103:8080:8080/tcp"این آدرس را با دستور tailscale ip -4 روی میزبان پیدا کنید. در اینجا، bind کردن کنترل قویتری نسبت به قانون فایروال است، زیرا پورت اصلاً روی رابط عمومی باز نمیشود. این کار همچنین مشکل انتشار مستقیم پورتها توسط داکر و دور زدن ufw را برطرف میکند.
دسترسی خروجی جایی است که FIREWALL_OUTBOUND_SUBNETS اهمیت پیدا میکند. اگر یک container نیاز به برقراری ارتباط با یک peer دارد، آدرس آن peer را اضافه کنید و به جای کل محدوده /10، از یک /32 برای هر peer استفاده کنید. نامهای MagicDNS داخل container ترجمه (resolve) نمیشوند، زیرا container از resolver میزبان استفاده نمیکند؛ بنابراین از آدرس عددی 100.x استفاده کنید یا آن را با یک خط extra_hosts ثابت کنید. همین موضوع هنگام اجرای سرور کنترل Tailscale شخصی با Headscale نیز صدق میکند.
یک فایل compose کامل برای ساختار رایج
یک کلاینت دانلود پشت VPN، دو رابط کاربری وب که فقط روی tailnet پاسخ میدهند، و یک کانتینر که دیتابیس PostgreSQL در حال اجرا روی میزبان را میخواند.
services:
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "127.0.0.1:8000:8000/tcp" # gluetun control server, host only
- "100.101.102.103:8080:8080/tcp" # qBittorrent UI, tailnet only
- "100.101.102.103:9696:9696/tcp" # Prowlarr UI, tailnet only
volumes:
- ./gluetun:/gluetun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
- SERVER_CITIES=Amsterdam
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
- TZ=Europe/Amsterdam
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- /srv/downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- PROWLARR__POSTGRES__HOST=host.docker.internal
- PROWLARR__POSTGRES__PORT=5432
- PROWLARR__POSTGRES__USER=prowlarr
- PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
- PROWLARR__POSTGRES__MAINDB=prowlarr-main
- PROWLARR__POSTGRES__LOGDB=prowlarr-log
volumes:
- ./prowlarr:/config
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedبه جای نام محصولات، به الگوی موجود در فایل توجه کنید. هر دو رابط کاربری روی gluetun منتشر شده و به آدرس tailnet میزبان متصل شدهاند، بنابراین آنها فقط روی Tailscale پاسخ میدهند و نه جای دیگر. فقط Prowlarr دارای خط extra_hosts است، زیرا Prowlarr کانتینری است که host.docker.internal را resolve میکند. FIREWALL_OUTBOUND_SUBNETS دو آدرس مجزا را نام میبرد: آدرس bridge داکرِ میزبان، تا Prowlarr بتواند اتصال دیتابیس را برقرار کند، و یک peer در tailnet.
سرور PostgreSQL عمداً در این فایل وجود ندارد. این سرور روی VPS به عنوان یک سرویس سیستمی معمولی در حال اجراست و روی 172.17.0.1:5432 گوش میدهد. این همان لایهبندی موجود در یک stack از نوع arr روی Docker Compose است، با این تفاوت که دیتابیس به خارج از داکر منتقل شده است.
کلید خصوصی WireGuard را خارج از فایل compose نگه دارید. ${WIREGUARD_PRIVATE_KEY} اطلاعات را از یک فایل .env در کنار خود میخواند؛ الگویی که در فایلهای env و secretها برای Docker Compose پوشش داده شده است. بند condition: service_healthy از healthcheckای استفاده میکند که ایمیج gluetun به صورت پیشفرض ارائه میدهد، بنابراین تا زمانی که تونل وضعیت up را گزارش نکند، هیچچیز شروع نمیشود. بررسیهای سلامت در Compose ساختار کلی آن را توضیح میدهند.
انتشار روی تمام آدرسها به جای فقط tailnet
پیشوند آدرس و bindهای پورت را از 0.0.0.0 حذف کنید، که شامل IP عمومی VPS میشود. این کار را فقط پشت فایروالی که کنترل میکنید انجام دهید و ابتدا یادداشت ufw در بالا را مطالعه کنید.
ports:
- "8080:8080/tcp"تأیید عبور ترافیک از تونل
درخواست یکسانی را دو بار اجرا کنید؛ یک بار از داخل namespace و بار دیگر از روی host، سپس نتایج را مقایسه کنید.
docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.orgدر اجرای اول باید آدرس خروجی ارائهدهنده VPN شما نمایش داده شود. در اجرای دوم باید آدرس VPS شما نمایش یابد. اگر این دو آدرس یکسان باشند، ترافیک container از طریق تونل عبور نمیکند و تا زمانی که این مشکل برطرف نشود، سایر اصلاحات این راهنما بیفایده خواهند بود.
جدول مسیریابی (routing table) نشان میدهد چه ترافیکی از تونل عبور میکند و چه ترافیکی خیر.
docker run --rm --network=container:gluetun alpine:3.22 ip route showمسیر پیشفرض (default route) باید به رابط تونل یعنی tun0 اشاره کند. در زیر آن باید به ازای هر ورودی در FIREWALL_OUTBOUND_SUBNETS، یک مسیر مشاهده کنید که به gateway پل Docker اشاره دارد. هر مسیر دیگری که از طریق eth0 خارج شود، ترافیکی است که VPN را دور میزند.
سرور کنترلی Gluetun همان IP عمومی را روی پورت 8000 و در آدرس /v1/publicip/ip گزارش میدهد. نسخههای جدید نیاز دارند که برای مسیرهای سرور کنترلی احراز هویت تنظیم کنید، بنابراین پیش از تکیه بر آن، این مورد را پیکربندی کنید.
نشت ناشی از یک زیرشبکه (subnet) اشتباه
FIREWALL_OUTBOUND_SUBNETS حفرهای است که شما عمداً در فایروال ایجاد میکنید، بنابراین اندازه این حفره برابر با میزان ریسک آن است. چهار روش برای بیش از حد بزرگ کردن این حفره وجود دارد:
0.0.0.0/0همه چیز را به خارج از تونل میفرستد. دو بررسی IP در بالا، این مورد را در همان اجرای اول شناسایی میکنند، زیرا هر دو آدرس یکسانی را برمیگردانند.- دامنهای وسیعتر از هدف. باز کردن
10.0.0.0/8برای دسترسی به یک ماشین در10.0.1.7، تمام آدرسهایی را که یک همتای تورنت (torrent peer) ممکن است در آن محدوده تبلیغ کند نیز باز میکند. از10.0.1.7/32استفاده کنید. - دامنهای که با آدرسهای خودِ تونل همپوشانی دارد. مستندات gluetun هشدار میدهد که این کار باعث میشود gluetun ترافیک VPN را از طریق bridge ارسال کند که این امر باعث اختلال در port forwarding میشود. پیش از باز کردن هر محدوده خصوصی، مقدار
WIREGUARD_ADDRESSESخود را بررسی کنید. 100.64.0.0/10برای Tailscale. این مقدار تقریباً چهار میلیون آدرس را باز میکند تا بتوان به یک peer دسترسی داشت. peerهای مورد نیاز خود را به صورت ورودیهای/32لیست کنید.
به یاد داشته باشید که این تنظیم، کل namespace را پوشش میدهد. باز کردن یک زیرشبکه برای اینکه یک indexer بتواند به یک سرویس میزبان دسترسی پیدا کند، همان زیرشبکه را برای کلاینت تورنتی که از آن namespace استفاده میکند نیز باز میگذارد. پس از هر تغییر در این متغیر، بررسی IP عمومی را دوباره انجام دهید، زیرا این تنها آزمایشی است که نشان میدهد آیا تغییر اعمالشده همان چیزی بوده که مد نظر شما بوده است یا خیر.
وقتی gluetun را ریاستارت میکنید چه چیزی از کار میافتد
از آنجا که gluetun مالک namespace است، چرخه حیات آن با چرخه حیات namespace یکی است. شروع کردن یک کانتینر وابسته در حالی که gluetun خاموش است، بلافاصله با شکست مواجه میشود:
Error response from daemon: cannot join network of a non running containerریاستارت کردن gluetun در جای خود، نوعی شکست بیسروصدا است. کانتینرهای وابسته به کار خود ادامه میدهند در حالی که namespaceای که به آن متصل شدهاند در زیر آنها بازسازی میشود؛ در نتیجه docker ps گزارش میدهد که همه چیز سالم است، اما هیچ پاسخی دریافت نمیشود. پس از هر تغییری در سرویس gluetun، بهجای ریاستارت کردن یک بخش، کل گروه را بازسازی کنید.
docker compose up -d --force-recreateهمین موضوع در مورد بهروزرسانی imageها نیز صدق میکند. دریافت (pull) یک image جدید برای gluetun و بازسازی تنها همان سرویس، باعث میشود سایر سرویسها به namespaceای اشاره کنند که دیگر وجود ندارد.
FAQ
چرا Docker خطای "port publishing and the container type network mode" را نمایش میدهد؟
به این دلیل که یک بلوک ports: روی سرویسی قرار دارد که network_mode: service:gluetun را نیز تنظیم کرده است. انتشار یک پورت، یک قانون NAT اضافه میکند که پورت میزبان را به فضای نام شبکهٔ کانتینر هدایت میکند، در حالی که کانتینر در این حالت چنین فضایی ندارد. بلوک ports: را از آن سرویس حذف کنید و نگاشت مشابه را به سرویس gluetun اضافه کنید. شمارهٔ پورت ثابت میماند، زیرا برنامه همچنان در فضای نام مشترک روی همان پورت گوش میدهد.
سایر کانتینرها چگونه به سرویسی که پشت gluetun قرار دارد دسترسی پیدا میکنند؟
کانتینرهای موجود در یک فضای نام از طریق 127.0.0.1 به یکدیگر متصل میشوند. کانتینرهای خارج از آن از نام سرویس gluetun استفاده میکنند، بنابراین http://gluetun:8080 کار میکند در حالی که http://qbittorrent:8080 کار نمیکند. کانتینر برنامه هیچ آدرسی در هیچ شبکهٔ Docker ندارد، بنابراین سرور DNS تعبیهشده چیزی برای حل کردن (resolve) برای نام آن ندارد. تا زمانی که هر دو کانتینر در یک شبکهٔ Compose مشترک باشند، نیازی به انتشار پورت برای این کار نیست.
در FIREWALL_OUTBOUND_SUBNETS چه چیزی باید قرار دهم؟
فقط آدرسهایی که کانتینر پشت gluetun باید اتصال به آنها را آغاز کند، آن هم با دقیقترین حالت ممکن. یک ماشین تکی یک /32 است. دو ورودی رایج، میزبان Docker در 172.17.0.1/32 و یک /32 برای هر همتای Tailscale است که با آن تماس میگیرید. هرگز 0.0.0.0/0 را اضافه نکنید و هرگز محدودهای که با آدرسهای تونل VPN شما همپوشانی دارد، وارد نکنید. اتصالات ورودی به یک پورت منتشرشده نیازی به ورودی در اینجا ندارند.
چرا کانتینر نمیتواند نامهای Tailscale MagicDNS من را resolve کند؟
MagicDNS با اشاره کردن resolver میزبان به سرور DNS شرکت Tailscale کار میکند و کانتینر از resolver میزبان استفاده نمیکند. کانتینر از هر آنچه در /etc/resolv.conf تنظیم شده استفاده میکند که در پشت gluetun، همان تنظیمات DNS مربوط به gluetun است. این موضوع را با docker exec <container> cat /etc/resolv.conf تأیید کنید. از آدرس عددی 100.x همتا استفاده کنید یا نام را با یک ورودی extra_hosts روی آن کانتینر ثابت کنید.
چگونه تأیید کنم که ترافیک همچنان از طریق VPN عبور میکند؟
یک درخواست از داخل فضای نام و همان درخواست را از روی میزبان اجرا کنید، سپس پاسخها را مقایسه کنید. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org باید آدرس خروجی ارائهدهندهٔ VPN شما را برگرداند، در حالی که curl -s https://api.ipify.org روی VPS باید آدرس همان VPS را برگرداند. اگر دو پاسخ یکسان باشند، یعنی تونل ترافیک کانتینر را حمل نمیکند. این بررسی را پس از هر تغییر در FIREWALL_OUTBOUND_SUBNETS دوباره انجام دهید.