چگونه فقط ترافیک کانتینرهای Docker را از OpenVPN عبور دهیم
میزبان را روی uplink معمولیاش نگه دارید و یک زیرشبکه کانتینر را در تونل بگذارید: ترتیب NAT، قواعد مبدأ، iroute در OpenVPN و بررسیهای MSS.
بستهٔ کانتینر هرگز به tun0 نمیرسد و قابلیتهای اضافی روی کانتینر این را تغییر نمیدهد. بین مهمان و تونل دو جستوجوی مسیر قرار دارد: یکی درون فضای نام شبکهٔ مهمان، یکی روی میزبان پس از آنکه قاب روی پل فرود میآید، و دومی از جدولی متفاوت با قواعد تطبیق متفاوت میپرسد. چیدمان مرجع من یک میزبان Linux است که OpenVPN کلاینت را با 10.8.0.10 روی tun0 اجرا میکند، یک پل Docker روی 172.18.0.0/16، و یک نشانی خصوصی که فقط سمت تونل به آن میرسد. هدف باریک میماند: آن زیرشبکهٔ مهمان از تونل خارج میشود در حالی که میزبان uplink معمولیاش را نگه میدارد.
اجرای کلاینت VPN درون کانتینر طراحی دیگری است، نه میانبر. آنگاه کانتینر مالک تونل و اعتبارنامههایش است و کار به Dockerfile منتقل میشود نه اینکه در iptables بماند.
ارسال، مسیر را بر اساس مقصد انتخاب میکند، سپس NAT مبدأ را بازنویسی میکند
وقتی net.ipv4.ip_forward=1 تنظیم شده باشد، بستهٔ ارسالشده فقط بر اساس مقصد، در جدول اصلی مسیریابی میشود. مبدأ هیچ نقشی در این تصمیم ندارد. تنها پس از انتخاب مسیر است که هسته POSTROUTING را اجرا میکند و همانجاست که قاعدهٔ خود Docker مینشیند: -s 172.18.0.0/16 ! -o br-appnet -j MASQUERADE. هر چیزی که پل را به سوی رابط دیگری ترک کند ترجمه میشود و نشانیای که میگیرد به رابطی بستگی دارد که مسیر آن را به آنجا فرستاده است.
این قاعده بیشتر نشانههایی را که از من میپرسند توضیح میدهد. اگر جدول اصلی مسیری برای مقصد خصوصی نداشته باشد، بسته با نشانی فیزیکی میزبان از uplink خارج میشود. ارائهدهندهٔ شما مقصد را میاندازد، یا سایت دور مبدأ RFC 1918 را که هرگز انتظارش را نداشت میاندازد، و کانتینر در حالی که تونل برپا و سالم است به پایان زمان میرسد. ضبط روی tun0 هیچ چیز نشان نمیدهد و این همان نشانه است. اگر مسیر واقعاً به tun0 اشاره کند، MASQUERADE مبدأ را به 10.8.0.10 بازنویسی میکند و همان کانتینر بدون اینکه چیزی را لمس کنید کار میکند.
جستوجویی را بخواهید که مبدأ مهمان را نام میبرد. بدون from و iif میزبان بهجای سوکت خودش پاسخ میدهد و مسیری را گزارش میکند که بستهٔ ارسالشده هرگز آن را نمیپیماید:
ip route get 192.168.11.222 from 172.18.0.5 iif br-appnetاگر آن پاسخ uplink را نام ببرد، تونل در تصویر نیست و هیچ تنظیمی در سمت تونل کمکی نمیکند.
مهمان را به نشانی خود تونل منتقل کنید
کوچکترین تغییر کارساز، بازنویسی صریح مبدأ در مسیر خروج است. آن را درج کنید نه اینکه بیفزایید: Docker از قبل در سر POSTROUTING نشسته است، پس قاعدهای که با -A افزوده شود بعد از قاعدهٔ Docker ارزیابی میشود و هرگز اجرا نمیشود.
iptables -t nat -I POSTROUTING 1 -s 172.18.0.0/16 -o tun0 -j SNAT --to-source 10.8.0.10دو شرط تعیین میکند که این دوام بیاورد. نشانی باید همان باشد که کلاینت واقعاً دارد، پس آن را با ورودیای برای هر کلاینت که ifconfig-push 10.8.0.10 255.255.255.0 را در بر دارد سنجاق کنید؛ قاعدهای که نشانیای را نام ببرد که هرگز تخصیص نیافته، حذفهای خاموش میسازد نه خطا. و سمت دور باید آن مبدأ را بپذیرد و پاسخها را بازگرداند، از اینرو فهرست مجاز آنجا نشانی تونل را نام میبرد نه زیرشبکهٔ مهمان. وقتی نشانی کلاینت پویاست، -j MASQUERADE با -o tun0 هر چه رابط دارد را دنبال میکند، به بهای سیاست دوری که نمیتوانید برایش مقدار ثابت بنویسید.
فیلتر کردن نیمهٔ دیگر است. Docker سیاست FORWARD را روی drop میگذارد و در هر راهاندازی مجدد سرویس، زنجیرههایی را که مالک آن است پاک میکند، پس سیاست محلی را در DOCKER-USER بگذارید که زنده میماند:
iptables -I DOCKER-USER -i br-appnet -o tun0 -j ACCEPT
iptables -I DOCKER-USER -i tun0 -o br-appnet -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTقاعدهٔ بازگشت مهم است: پاسخی که روی tun0 برای اتصالی که پل باز کرده میرسد، فقط به این دلیل که جهت خروج پذیرفته شده بود پذیرفته نمیشود.
میزبان را با قاعدهٔ مبدأ مستقیم نگه دارید
وقتی تمام پل باید از تونل استفاده کند ولی هیچ چیز دیگری روی میزبان نباید، از NAT بگذرید و به آن مبدأ جدول خودش را بدهید. نشانیهای مهمان آنگاه سرتاسر نمایان میمانند.
echo "200 vpnbox" >> /etc/iproute2/rt_tables
ip rule add from 172.18.0.0/16 lookup vpnbox pref 1000
ip route add default dev tun0 table vpnboxقواعد پیش از جدول اصلی بررسی میشوند و from با مبدأ تطبیق مییابد. بستههای بیرونی خود تونل نشانی فیزیکی میزبان را حمل میکنند، پس هرگز با from 172.18.0.0/16 تطبیق نمییابند و تونل نمیتواند خودش را به درون خودش مسیریابی کند. همین خوداستثنا بودن است که چیدمان را پایدار میکند.
مسیر بازگشت اکنون به کمک نیاز دارد. سمت دور یک 172.18.0.x ترجمهنشده میبیند و دلیلی ندارد بداند کجاست. روی سرور، پروندهٔ هر کلاینت به iroute 172.18.0.0 255.255.0.0 نیاز دارد تا جدول داخلی سرور آن پیشوند را به این کلاینت بدهد، و به route 172.18.0.0 255.255.0.0 که هل داده شود تا کلاینتهای دیگر بدانند وجود دارد.
دو بررسی پیش از اعتماد. اجرا کنید ip route show default: با redirect-gateway def1 جدول 0.0.0.0/1 و 128.0.0.0/1 را از طریق tun0 نگه میدارد، و میزبان اصلاً مستقیم نیست، چون آن نیمهپیشفرضها بدون جایگزینی default پیروز میشوند. قواعدی که با ip rule add افزوده شدهاند نیز در راهاندازی مجدد ناپدید میشوند، پس آنها را در یک اسکریپت up ببرید.
اندازهٔ بسته شکستی است که دوم با آن روبهرو میشوید
یک رابط پل با 1500 بایت بیشتر از توانش به تونل میدهد وقتی سرآیندهای بیرونی UDP و IP افزوده شوند. تبادلهای کوچک عبور میکنند، پس DNS حل میشود و دستدهی TLS کامل میشود، سپس یک بارگذاری بزرگتر بدون هیچ خطایی در جایی معلق میماند. فرستنده همان اندازهٔ قطعه را تکرار میکند چون پیام ICMP دربارهٔ نیاز به قطعهبندی، که به آن میگفت کوچک شود، هرگز نمیرسد.
آن را در رابط، در MSS یا هر دو درست کنید. گیرهانداختن نیازی به همکاری مهمان ندارد:
ip link set dev tun0 mtu 1400
iptables -t mangle -A FORWARD -o tun0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtuگیرهانداختن فقط TCP را بازنویسی میکند. UDP، شامل QUIC، به جای آن به رابط پایینتر مهمان نیاز دارد، با --opt com.docker.network.driver.mtu=1400 هنگام ساخت شبکه یا کلید compose متناظر.
DNS و بررسیهایی که ارزش تکرار دارند
حل نام از همان مسیر پیروی میکند. حلکنندهٔ تعبیهشدهٔ Docker در 127.0.0.11 به سرورهای پیکربندیشدهٔ میزبان ارسال میکند، پس کانتینر هر چه میزبان استفاده میکند را به ارث میبرد. وقتی منطقهٔ خصوصی فقط توسط حلکنندهای که از تونل قابل دسترسی است پاسخ داده شود، مهمان را به آن نشانی بگردانید و با getent hosts درون کانتینر آزمایش کنید نه روی میزبان.
- درخواست شکستخورده را درون مهمان اجرا کنید؛ آزمایش سمت میزبان نشانی مبدأ دیگری به کار میبرد و مشکل را پنهان میکند.
- شمارندههای قواعد NAT و فیلتر را که افزودهاید بخوانید. قاعدهای با صفر تطبیق، خطای مسیریابی است نه خطای سیاست.
- تأیید کنید که نشانی تونل در قاعدهٔ SNAT شما همان است که کلاینت واقعاً دارد.
- MTU مهمان را کم کنید یا MSS را گیره بزنید پیش از آنکه سرور را برای توقف سرزنش کنید.
فهرست را پایین بروید و در نخستین شکست بایستید. همان لایه است که باید درست شود؛ هر چیزی که پایینتر افزوده شود فقط مرزی را که میشکند پنهان میکند.