بیشتر یک صبح را این بهار صرف دنبال کردن کانتینری کردم که نمی‌توانست حتی یک نام داخلی را حل کند، در حالی که مرورگر کنارش ویکی را بدون شکایت باز می‌کرد. تونل بالا بود، مرورگر این را ثابت می‌کرد، و کانتینر در فضای نام شبکه دیگری با جدول مسیر متفاوت و ریزالور متفاوت قرار داشت. آنچه در ادامه می‌آید ترتیبی است که اکنون بررسی می‌کنم: ابتدا جدول‌های مسیر، دوم حل نام، سوم ارسال، و در آخر پلتفرم میزبان. در نخستین گامی که از قبل شکست می‌خورد بایستید، چون افزودن قواعد در پایین‌تر پشته مرز را پنهان می‌کند نه اینکه آن را جابه‌جا کند.

پیش از تغییر یک قاعده، سه جدول مسیر را مقایسه کنید

کانتینر روی پل پیش‌فرض Docker یک مسیر پیش‌فرض دارد که به دروازه docker0 اشاره می‌کند و هیچ ایده‌ای از وجود tun0 ندارد. میزبان tun0 را دارد، مسیرهایی که VPN هل داده، و شاید قاعده‌ای سیاستی که ترافیک آزمون را از رابطی متفاوت از آنچه انتظار دارید بیرون می‌فرستد. WSL یک جدول سوم پشت آداپتور مجازی خودش اضافه می‌کند. پیش از ویرایش هر یک، هر سه را چاپ کنید.

ContextCommandWhat a wrong answer looks like
Hostip route get 10.8.0.20Leaves a physical interface instead of tun0
Containerip routeOnly 172.17.0.0/16 via 172.17.0.1
Container DNScat /etc/resolv.confA public resolver, or an upstream the tunnel never pushed
WSLip addr; ip routeOnly the WSL subnet, no Windows VPN adapter

اگر تنها مسیر کانتینر پل باشد و tun0 هرگز در فضای نامش ظاهر نشود، هیچ‌چیز در پایین کمک نمی‌کند: هیچ تغییر سرور نامی مسیر نمی‌سازد و هیچ مسیری هویتی برای تونل نمی‌سازد که همتا بپذیرد. ترتیب مهم است چون شکست DNS و شکست مسیر از درون پوسته یکسان به نظر می‌رسند.

ریزالور را به تونل اشاره دهید نه به یک ریزالور عمومی

روی میزبان، resolvectl status سرورهای DNS را به تفکیک پیوند فهرست می‌کند، و readlink -f /etc/resolv.conf می‌گوید فایل همان استاب systemd است، فایل NetworkManager، یا چیزی که کلاینت یک فروشنده بازنویسی کرده. درون کانتینر روی شبکه‌ای که کاربر تعریف کرده، /etc/resolv.conf نام می‌برد 127.0.0.11، ریزالور توکار Docker را، که به هر بالادستی که دیمن با آن پیکربندی شده بود ارسال می‌کند. اما کانتینر روی پل پیش‌فرض هنگام ساخت نسخه‌ای از فایل میزبان گرفت و آن را نگه می‌دارد.

همان نسخه دلیل آن است که تونلی که پس از شروع کانتینر بالا می‌آید، کانتینر را وادار می‌کند از بالادست قدیمی بپرسد. یا ریزالور VPN را صریحاً با docker run --dns 10.8.0.53 پاس دهید، یا "dns": ["10.8.0.53"] را در /etc/docker/daemon.json تنظیم کنید و دیمن را دوباره راه‌اندازی کنید، یا وقتی تونل بالا آمد کانتینر را از نو بسازید. ساختن دوباره معمولاً ارزان‌تر از اشکال‌زدایی ریزالوری است که یک ساعت پیش درست بود.

جایگزینی با یک ریزالور عمومی همان تعمیری است که نشانه را حفظ می‌کند. اینترنت را حل می‌کند و در هر ناحیه خصوصی شکست می‌خورد، و می‌تواند بی‌صدا از فیلتری که تونل برای اعمال آن وجود دارد عبور کند. اگر تلاش کنید درون /etc/resolv.conf یک کانتینر در حال اجرا بنویسید، انتظار Device or resource busy را داشته باشید: Docker آن فایل را bind-mount می‌کند، پس ویرایش نخست به unmount نیاز دارد. تنظیم --dns هنگام ساخت همان ترتیبی است که از راه‌اندازی دوباره جان سالم به در می‌برد.

وقتی کانتینرها تونل را قرض می‌گیرند، ارسال و ترجمه کنید

با پل پیش‌فرض، میزبان یک مسیریاب میان docker0 و tun0 است و به‌طور پیش‌فرض هیچ‌یک از این دو کار را انجام نمی‌دهد. ارسال را روشن کنید و آدرس مبدأ را ترجمه کنید تا همتا آدرس تونل میزبان را ببیند نه آدرسی که مسیر بازگشتی به آن ندارد (tun0 را با نامی که کلاینت شما به رابط می‌دهد جایگزین کنید):

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
iptables -A FORWARD -i docker0 -o tun0 -j ACCEPT
iptables -A FORWARD -i tun0 -o docker0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

قواعد FORWARD روی میزبان‌هایی مهم‌اند که سیاست ارسالشان DROP است. Docker زنجیره‌های خودش را وارد می‌کند، پس با بسته آزمایش کنید نه با خواندن iptables -S: از کانتینر، ip route get 10.8.0.20 باید دروازه پل را نام ببرد، و nc -vz 10.8.0.20 443 باید در تونل وصل شود یا زمانش تمام شود نه در ریزالور.

بدون MASQUERADE همتا به مسیر بازگشتی به 172.17.0.0/16 نیاز دارد که بیشتر VPNهای دسترسی راه دور آن را هل نمی‌دهند. اینجا همچنین جایی است که کلاینت مبتنی بر سیاست نقشه را خراب می‌کند: اگر تنها ترافیک UIDها یا cgroups مشخص را بگیرد، قواعد ارسال شما هرگز بسته را درون تونل نمی‌بینند، و اصلاح به سیاست مسیریابی کلاینت تعلق دارد نه به iptables. قواعدی که دستی افزوده شوند در راه‌اندازی دوباره از بین می‌روند، پس آن‌ها را در یک واحد systemd یا جدول netfilter-persistent بگذارید و پس از راه‌اندازی دوباره دوباره آزمایش کنید، چون تونلی که دیر بالا می‌آید می‌تواند آن‌ها را بشوید.

وقتی هدف جداسازی است، تونل را درون کانتینر بگذارید

اگر یک کانتینر باید از گره مشخصی خارج شود، کلاینت را در همان کانتینر اجرا کنید. به --cap-add NET_ADMIN و --device /dev/net/tun نیاز دارد، و سپس tun0، مسیرها و دیوار آتش خودش را دارد. اگر کانتینرهای دیگر از آن عبور کنند، به ارسال و قاعده MASQUERADE درون کانتینر هم نیاز دارد، و آن همتاها به مسیر یا پروکسی صریحی نیاز دارند که به آن اشاره کند؛ پل پیش‌فرض Docker آن مسیر را برای شما اضافه نمی‌کند.

شبکه میزبان، docker run --network host، نسخه ارزان است: کانتینر به فضای نام میزبان می‌پیوندد و مسیرهای تونل را مستقیم می‌بیند. همچنین جداسازی شبکه را دور می‌ریزد، به تصادم پورت‌ها دعوت می‌کند و در عمل گزینه‌ای Linux است. پورت منتشرشده جایگزین نیست. -p 8080:80 یک سرویس را از میزبان در دسترس می‌کند؛ آدرس‌های داخلی 10.x را از درون کانتینر در دسترس نمی‌کند، و قاطی کردن این دو همان راهی است که یک مشکل مسیریابی به یک پورت پایگاه‌داده افشاشده تبدیل می‌شود.

شکست نام را از یک زیرشبکه هم‌پوشان جدا کنید

پل پیش‌فرض Docker برابر 172.17.0.0/16 است، و بسیاری از استخرهای VPN شرکتی درون 172.16.0.0/12 قرار دارند. وقتی تونل مالک آن محدوده شود، ترافیکی که برای کانتینر در نظر گرفته شده می‌تواند به‌جای آن به درون تونل مسیریابی شود. نام‌ها شکست می‌خورند، خطا شبیه DNS است، و مشکل آدرس‌هاست. مقصد را بررسی کنید، نه ریزالور را: ip route get 172.17.0.5 روی میزبان، سپس پیشوند پل را با استخر VPN و با docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}' مقایسه کنید.

تعمیر این است که یکی از محدوده‌ها را جابه‌جا کنید: یا یک استخر آدرس پیش‌فرض متفاوت برای Docker در daemon.json یا یک استخر متفاوت روی متمرکزکننده. افزودن سرور نام دیگر اینجا هیچ کاری نمی‌کند، و ssh -b هم همین‌طور: بستن کلاینت به یک رابط آدرس مبدأ را عوض می‌کند، نه مسیر مقصدی که به درون تونل اشاره دارد. تقسیم کار سریع: نامی که روی میزبان حل می‌شود ولی در کانتینر نه، مشکل ریزالور است، در حالی که اتصال مستقیم IP که در کانتینر گیر می‌کند ولی روی میزبان کار می‌کند، مشکل مسیر یا زنجیره ارسال است.

WSL و Docker Desktop از قواعد Linux پیروی نمی‌کنند

در حالت NAT پیش‌فرضش، WSL پشت آداپتور مجازی خودش می‌نشیند و مسیرهای داخلی یا پسوندهای DNS مربوط به VPN ویندوز را دریافت نمی‌کند، به همین دلیل مرورگر کار می‌کند و ping درون توزیع کار نمی‌کند. شبکه آینه‌ای، که در بیلدهای جدید Windows 11 موجود است، در عوض رابط‌های میزبان را به اشتراک می‌گذارد، اما باید در .wslconfig فعال شود و توزیع با wsl --shutdown دوباره راه‌اندازی شود. حالت فعال را با wslinfo --networking-mode پیش از اعمال تعمیر تأیید کنید، چون پاسخ NAT برای حالت آینه‌ای غلط است و برعکس.

Docker Desktop یک لایه دیگر اضافه می‌کند. کانتینرهای Linux روی Windows پشت پیاده‌سازی NAT که تاریخاً vpnkit نامیده می‌شود می‌نشینند، و مسیرشان با میزبان بومی Linux متفاوت است. کانتینرهای Windows از طریق سوئیچ مجازی Hyper-V متصل می‌شوند، و یک کلاینت شرکتی می‌تواند ترافیک آن سوئیچ را مسدود کند، بنابراین کانتینرهای Linux روی همان ماشین ممکن است به شبکه برسند در حالی که کانتینرهای Windows هیچ ندارند. سوئیچ خارجی متصل به آداپتور فیزیکی راه‌حل معمول است، و هویت شبکه کانتینر را به روش‌هایی تغییر می‌دهد که سیاست ممکن است اجازه ندهد.

پیش از نوشتن قاعده دیگری، چهار لایه را به ترتیب طی کنید و هر جا یکی از آن‌ها غلط بود بایستید:

  • درون کانتینر: آیا ip route به پیشوند خصوصی می‌رسد، و آیا /etc/resolv.conf بالادستی را نام می‌برد که آن را حل می‌کند؟
  • روی میزبان: آیا ارسال روشن است، آیا NAT به رابط تونل بسته شده، و آیا زنجیره ارسال پل به تونل را می‌پذیرد؟
  • روی همتا: آیا مسیر بازگشتی می‌داند، یا به NAT تکیه کرده‌اید تا زیرشبکه کانتینر را پنهان کند؟
  • روی پلتفرم: آیا WSL در حالت NAT است یا آینه‌ای، و کانتینر Linux است یا Windows؟

هر آزمون را در کانتینر یا پوسته تازه دوباره اجرا کنید. فرایند دیرزیستی که پاسخ کش‌شده یا اتصال برقرارشده دارد پس از درست شدن پیکربندی هم شکست می‌خورد، و آن شکست دقیقاً شبیه تعمیری است که کار نکرد.

مقالات مرتبط