دامنه و فایروال؛ چرا سرور از داخل سالم است ولی از اینترنت باز نمیشود؟
در این درس ۶ بخش
پس از درس قبل، داخل سرور میتوانیم با Host صحیح از Nginx پاسخ بگیریم. اکنون مسئله عوض میشود: کاربر بیرون از سرور چگونه مقصد را پیدا کند و آیا مسیر شبکه تا آن باز است؟ دامنه، رکورد DNS، فایروال میزبان و فایروال ارائهدهنده چهار نام برای یک چیز نیستند. هرکدام تصمیم متفاوتی میگیرند و شواهد متفاوتی دارند. در این درس آنها را یکییکی تنظیم و آزمایش میکنیم. دستگاه بیرونی میتواند رایانهٔ ویندوزی شما با PowerShell باشد؛ سرور همان Ubuntu آزمایشی است. کنار هر بلوک فرمان محل اجرا مشخص شده است. اگر ماشین شما از قبل میزبان سایت یا قوانین شبکهٔ دیگران است، قواعد این تمرین را مستقیم روی آن اعمال نکنید؛ وضعیت موجود را با مسئول سرویس تطبیق دهید یا یک ماشین مستقل بسازید.
اگر معنی دامنه و IP را فراموش کردهاید، ابتدا بخش شبکهٔ لینوکس مقدماتی را مرور کنید:
https://www.darsavand.com/courses/linux-server-from-zero?lesson=6f7aa53e-7468-4f60-9ef1-8cb867907928
برای روش تشخیص خطا از نام تا پاسخ برنامه نیز این درس لینوکس پیشرفته مرتبط است:
https://www.darsavand.com/courses/linux-advanced-administration?lesson=0e24bd50-d875-4979-8a88-69fea1b60aa9
اتصال این بخش به جاوااسکریپت روشن است: #fetch-http-json پس از پیداشدن مقصد و برقراری ارتباط میتواند پاسخ HTTP بگیرد. اگر نام حل نشود، برنامه هنوز به مرحلهٔ بررسی response.status نرسیده است. و اگر یک ۴۰۴ واقعی بگیرد، مسیر شبکه تا یک وبسرور طی شده؛ علت را نباید صرفاً «اینترنت قطع است» نامید. نامگذاری درست مسئله باعث میشود مرحلهٔ بعدی عیبیابی نیز روشن باشد.
DNS، رکورد A و AAAA را با مثال بفهمیم
دامنه نام قابل خواندن مقصد است؛ DNS سامانهای است که اطلاعات مربوط به این نام را در قالب رکورد پاسخ میدهد. رکورد A یک نام را به IPv4 و رکورد AAAA آن را به IPv6 مرتبط میکند. این رکوردها پورت برنامه را تعیین نمیکنند و خودشان وبسرور نصب نمیکنند. برای api زیر دامنهٔ خودتان، در پنلی که DNS واقعی آن دامنه را مدیریت میکند رکورد میسازید؛ پنل خرید دامنه و پنل DNS الزاماً یک جا نیستند. مقدار باید نشانی عمومی واقعی همین سرور از پنل ارائهدهنده باشد، نه 127.0.0.1 و نه نشانی خصوصی شبکهٔ داخلی. در این آموزش 203.0.113.10 فقط نمونهٔ مستنداتی است و مسیر اینترنتی دستگاه شما نیست. قبل از نوشتن هر دستور، نام واقعی و IP واقعی را در یادداشت محیط ثبت کنید تا کپیکردن مثال به مقصد نادرست منجر نشود.
TTL مدت پیشنهادی نگهداری پاسخ در کش DNS است؛ کش یعنی نگهداشتن پاسخ برای استفادهٔ دوباره، تا هر درخواست از ابتدا به منبع اصلی نرود. به همین دلیل، اصلاح رکورد ممکن است در همهٔ دستگاهها همزمان دیده نشود. «انتشار DNS» یک کلید شروع واحد در کل اینترنت نیست. باید بدانید کدام resolver پاسخ میدهد و پاسخ authoritative چه بوده است. authoritative سروری است که اطلاعات آن ناحیهٔ DNS را نگه میدارد؛ resolver پاسخ را برای درخواستکننده پیدا و معمولاً کش میکند. برای شروع همین تفاوت کافی است؛ قرار نیست روی VPS خودتان DNS server جدید نصب کنید. یک نام را بهدرستی در ارائهدهندهٔ DNS موجود تنظیم میکنیم و خروجی مشاهدهشده را با مقدار مورد انتظار مقایسه میکنیم.
اول مسیر را بدون وابستگی به DNS عمومی آزمایش کنید
# روی رایانهٔ ویندوز، در PowerShell؛ دو مقدار را واقعی کنید.
$apiDomain = 'api.example.com'
$serverIp = '203.0.113.10'
curl.exe -i --connect-timeout 5 --max-time 15 --resolve "${apiDomain}:80:${serverIp}" "http://${apiDomain}/health"
Test-NetConnection -ComputerName $serverIp -Port 80در این آزمون، --resolve فقط برای همین اجرای curl نگاشت نام به IP را تعیین میکند؛ فایل hosts ویندوز یا رکورد واقعی دامنه را تغییر نمیدهد. Host همچنان نام api است، پس Nginx میتواند همان میزبان درس قبل را انتخاب کند. با فرض آمادهبودن فایروالها، انتظار HTTP ۲۰۰ و بدنهٔ سلامت است. Test-NetConnection نتیجهٔ اتصال TCP را نشان میدهد؛ TcpTestSucceeded برابر True به معنی سلامت منطق برنامه نیست، فقط اتصال آن پورت برقرار شده است. اگر پورت هنوز در پنل شبکه باز نشده، شکست این آزمون در این مرحله قابل انتظار است؛ نتیجه را ثبت و پس از تنظیم قواعد تکرار کنید. کپی همین فرمان در Bash خطا میدهد، چون شکل متغیرها متعلق به PowerShell است. curl.exe را صریح نوشتهایم تا با نام مستعار curl در بعضی نسخههای ویندوز اشتباه نشود.
ثبت رکورد واقعی و پرهیز از IPv6 نیمهکاره
در پنل DNS، نوع A، نام api یا نام کامل طبق روش همان پنل، و IPv4 عمومی سرور را وارد کنید. فقط اگر واقعاً IPv6 عمومی قابل مسیریابی روی همین ماشین، شنوندهٔ Nginx و قواعد شبکهٔ مناسب دارید، AAAA بسازید. یک AAAA قدیمی یا متعلق به سرور دیگر میتواند باعث شود بعضی کلاینتها به مقصد دیگری بروند، در حالی که آزمون IPv4 شما موفق است. اگر IPv6 ندارید، رکورد ساختگی برای کاملبودن ظاهری پنل اضافه نکنید؛ رکورد اشتباه را براساس دامنهٔ تحت اختیار خود اصلاح کنید. اگر از CDN یا پراکسی DNS استفاده میکنید، ممکن است پاسخ DNS نشانی لبهٔ آن سرویس باشد؛ معماری مستقیم این دوره را بدون توضیح با آن مخلوط نکنید. برای آزمون اولیه، وضعیت پروکسی و مسیر origin را آگاهانه ثبت کنید و قواعد امنیتی فعال سرویس دیگری را کورکورانه خاموش نکنید.
# روی ویندوز، همان PowerShell با apiDomain واقعی
Resolve-DnsName -Name $apiDomain -Type A
Resolve-DnsName -Name $apiDomain -Type AAAA
curl.exe -i --connect-timeout 5 --max-time 15 "http://${apiDomain}/health"رکورد A باید نشانی هدف مورد نظر را بدهد. اگر عمداً IPv6 ندارید، نبود پاسخ AAAA بهخودیخود خرابی نیست. پاسخ قبلی از کش را با آدرس فعلی پنل مقایسه کنید و زمان و resolver را در گزارش نگه دارید. اگر --resolve موفق ولی درخواست معمولی ناموفق است، به DNS یا مسیر متفاوتی که نام انتخاب کرده شک کنید؛ هنوز دلیلی برای تغییر کد #async-await در برنامه نداریم. اگر هر دو HTTP ۴۰۴ میدهند، نام میزبان و مسیر را بررسی کنید. خروجی HTML یک صفحهٔ خطا را بدون بررسی Content-Type با #json یکسان ندانید؛ JSON.parse روی هر متن دلخواه موفق نمیشود و خطای تجزیه، معمولاً پیامد دریافت قالب غیرمنتظره است، نه دلیل اصلی اشتباه مسیر.
دو فایروال؛ یکی در میزبان و دیگری در شبکهٔ ارائهدهنده
فایروال مجموعهٔ قواعد اجازه یا منع عبور ترافیک است. UFW در اوبونتو رابطی ساده برای مدیریت قواعد فایروال میزبان است؛ پنل VPS هم ممکن است فایروال شبکه یا security group جداگانه داشته باشد. اجازه در یکی، منع در دیگری را خنثی نمیکند. برای این معماری، دسترسی ورودی به پورت واقعی SSH، پورت ۸۰ و پورت ۴۴۳ لازم است. پورت ۳۰۰۰ برای اینترنت لازم نیست؛ برنامه فقط روی loopback گوش میدهد و Nginx همانجا به آن میرسد. بازکردن ۳۰۰۰ نه DNS را اصلاح میکند و نه خطای ۵۰۲ را. پیش از فعالکردن UFW، دسترسی کنسول بازیابی ارائهدهنده را بشناسید و نشست SSH فعلی را باز نگه دارید. وقتی راه دسترسی شما شبکه است، یک قاعدهٔ اشتباه میتواند راه اصلاح همان قاعده را نیز ببندد.
# روی اوبونتو، در نشست SSH فعلی
printf '%s\n' "$SSH_CONNECTION"
sudo ss -lntp
sudo ufw status verbose
sudo sshd -T | sed -n '/^port /p'SSH_CONNECTION معمولاً چهار مقدار دارد: IP و پورت کلاینت، سپس IP و پورت سمت سرورِ همین ارتباط. خروجی را منتشر نکنید؛ برای تشخیص محیط خودتان است. پورت آخر و شنوندهٔ واقعی ss را با تنظیمات دسترسی پنل مقایسه کنید. خروجی sshd -T تنظیم مؤثر daemon را گزارش میکند، اما در تنظیمات دارای socket activation یا نگاشت پورت شبکه، مشاهدهٔ شنونده و مسیر واقعی اتصال هم لازم است. اگر ارائهدهنده پورت بیرونی متفاوتی را به پورت داخلی نگاشت میکند، قاعدهٔ پنل و قاعدهٔ میزبان شمارههای مرتبط با همان لایه را دارند. از پیش فرض نگیرید همه جا پورت ۲۲ است. اگر UFW از قبل active و دارای قواعد ناشناخته است، کار را برای بررسی متوقف کنید؛ این آموزش مجوز پاککردن همهٔ قواعد قدیمی یا دسترسی سرویسهای دیگر نیست.
ترتیب امن قواعد؛ اجازهٔ SSH پیش از enable
# روی اوبونتوی آزمایشی؛ 22 را با پورت واقعی شنوندهٔ SSH عوض کنید.
SSH_PORT=22
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow "${SSH_PORT}/tcp"
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw status numberedقبل از گام بعد، در فایروال پنل نیز مسیر SSH واقعی را مجاز کنید و قواعد HTTP و HTTPS را برای مقصد درست قرار دهید. برای SSH میتوانید طبق امکانات ارائهدهنده دامنهٔ مبدأ مناسب مدیریت خودتان را محدود کنید، اما تغییر نشانی اینترنت یا مسیر VPN میتواند این فرض را عوض کند؛ بدون شناخت، قاعدهای که تنها مسیر ورود را میبندد ننویسید. برای وب عمومی، دسترسی کاربرهای مورد نظر به ۸۰ و ۴۴۳ لازم است؛ ACME در درس بعد از مبدأهای ثابت اعلامشده استفاده نمیکند. در ماشین تازه، دو دستور default سیاست پیشفرض ورود را منع و خروج را اجازه قرار میدهند؛ allowهای بعدی استثناهای مشخصاند. این تغییر روی سرور دارای قواعد و سرویسهای دیگر بدون بررسی انجام نمیشود. پیامهای اضافهشدن قواعد باید بدون خطا باشند. وضعیت inactive پیش از فعالسازی ممکن است فهرست کامل قواعد آمادهشده را مثل حالت active نشان ندهد؛ ثبت پیام موفق هر allow و بررسی قواعد پس از enable را با هم انجام دهید. هیچ دستور allow برای پورت ۳۰۰۰ در این مسیر نداریم.
# فقط پس از تأیید پورت واقعی SSH و قواعد پنل، روی اوبونتو
sudo ufw enable
sudo ufw status verboseپیام مربوط به اثر احتمالی روی اتصال SSH را بخوانید و تنها پس از بررسی پیششرطها ادامه دهید. نشست اول را نبندید. روی ویندوز، در یک پنجرهٔ دوم با همان حساب، کلید و پورت واقعی دوباره SSH بزنید؛ ورود موفق پنجرهٔ دوم شاهد مهمتر از زندهماندن نشست قدیمی است، چون بعضی قواعد ارتباط برقرارشده را حفظ میکنند. اگر ورود دوم شکست خورد، از همان نشست اول یا کنسول بازیابی، قاعدهٔ دقیق SSH و فایروال پنل را اصلاح کنید؛ درمان عمومی «همهٔ فایروالها را خاموش کن» نیست. پیکربندی گذرواژه یا کلید SSH را همزمان با این تغییر عوض نکنید، چون تشخیص علت شکست سختتر میشود. در این گام فقط مسیر عبور ترافیک را کنترل کردهایم؛ هویت حساب و مجوز sudo موضوع جدا هستند.
# روی ویندوز؛ IP را واقعی کنید و پورت SSH را مطابق اتصال خود انتخاب کنید.
Test-NetConnection -ComputerName $serverIp -Port 80
Test-NetConnection -ComputerName $serverIp -Port 443
Test-NetConnection -ComputerName $serverIp -Port 3000
curl.exe -i --connect-timeout 5 --max-time 15 "http://${apiDomain}/health"اکنون ۸۰ باید قابل اتصال باشد و HTTP سلامت ۲۰۰ بدهد. پیش از درس HTTPS، بسته به شنوندهٔ فعلی، شکست اتصال ۴۴۳ طبیعی است؛ مجازشدن قاعدهٔ فایروال خودبهخود برنامهای روی آن پورت ایجاد نمیکند. اتصال عمومی به ۳۰۰۰ باید ناموفق بماند، در حالی که curl داخلی به همان پورت موفق است. این دو خروجی با هم تناقض ندارند و دقیقاً جداسازی مورد نظر ما هستند. timeout میتواند از منع شبکه یا نبود مسیر باشد؛ connection refused معمولاً یعنی پاسخ رد اتصال از مسیر گرفتهایم. این پیامها را با کد HTTP مقایسه نکنید: برای ۴۰۳ یا ۵۰۲ ابتدا اتصال برقرار شده است. در برنامهٔ کلاینت، #try-catch برای شکست ارتباط مفید است، اما پاسخ HTTP ناموفق را هم باید صریح بررسی کنید.
تمرین تشخیص علت و پاسخ تشریحی
سناریو اول: GET داخلی از Nginx موفق است، --resolve از ویندوز timeout میشود و هیچ خط جدیدی در access log نیست. پاسخ راهنما: هنوز شاهد رسیدن درخواست به Nginx نداریم؛ IP عمومی، پورت، فایروال پنل و میزبان و شنونده را بررسی کنید. سناریو دوم: --resolve موفق است ولی درخواست نام دامنه به IP دیگری میرود. علت محتمل در DNS یا معماری پراکسی نام است؛ کد برنامه را بیدلیل تغییر ندهید. سناریو سوم: TCP پورت۸۰ باز است ولی /api/tasks وضعیت۴۰۳ برای POST میدهد. این رفتار درست محدودیت درس قبل است، نه لزوماً خرابی فایروال. تحویل این درس یک جدول واقعی از نام، IPهای A و AAAA، پورت شنونده، قواعد دو فایروال و چهار نتیجهٔ آزمون است. اگر فقط نوشتهاید «باز شد»، هنوز معلوم نیست کدام مسیر و کدام روش را آزمودهاید. پس از موفقیت، نشست مدیریتی اصلی را نگه دارید و برای HTTPS به درس بعد بروید.
منابع رسمی انگلیسی:
Ubuntu — Firewall: https://ubuntu.com/server/docs/how-to/security/firewalls/
Ubuntu — OpenSSH: https://ubuntu.com/server/docs/how-to/security/openssh-server/
Microsoft — Test-NetConnection: https://learn.microsoft.com/en-us/powershell/module/nettcpip/test-netconnection
Microsoft — Resolve-DnsName: https://learn.microsoft.com/en-us/powershell/module/dnsclient/resolve-dnsname
curl — Command line manual: https://curl.se/docs/manpage.html
IANA — Example domains: https://www.iana.org/help/example-domains
IETF — IPv4 documentation address blocks: https://www.rfc-editor.org/rfc/rfc5737
متن فارسی، سناریوهای خطایابی و تمرین: تألیف مستقل تحریریه درسآوند.
متن درسهای عمومی را بدون ورود بخوانید.