پرش به محتوای اصلی
زیرساخت و لینوکس
۶ دقیقه مطالعه

وقتی DNS قدیمی کاربر را به سایت اشتباه فرستاد: ماجرای یک ریدایرکت ۳۰۱ ماندگار

بعد از انتقال نیم‌سرور به CDN، آدرس سایت شخصی من را به سامانه حسابداری می‌برد. ردگیری علت: DNS کش‌شده روتر، default_server ضمنی در Nginx و 301 ماندگار.

سید محمد بهبهانی‌زاده
برنامه‌نویس فول‌استک

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

ماجرا و علامت‌ها

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

نکته گیج‌کننده این بود که همه ابزارها یک جواب نمی‌دادند. `nslookup` جواب درست و جدید را نشان می‌داد، ولی `Resolve-DnsName` در ویندوز جواب دیگری برمی‌گرداند. یعنی «DNS دامنه» برای دو ابزار روی یک دستگاه دو مقدار متفاوت داشت.

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

نکات کلیدی

  • تست‌های بیرونی سالم بود و فقط روی دستگاه من خطا دیده می‌شد.
  • دو ابزار روی یک ماشین دو جواب DNS متفاوت می‌دادند.
  • مقصد اشتباه یک محصول دیگر ما بود، نه یک سایت ناشناس.

ردگیری علت

اولین فرضیه این بود که تنظیمات CDN اشتباه است. این فرضیه با تست از بیرون رد شد، چون از شبکه‌های دیگر سایت کاملاً درست باز می‌شد و مشکل فقط در دستگاه من بود.

فرضیه دوم کش مرورگر بود. پاک کردن کش کمی رفتار را تغییر می‌داد، اما مشکل را حل نمی‌کرد. بعدها معلوم شد کش مرورگر بخشی از ماجرا بود ولی علت اول نبود.

نقطه عطف مقایسه دو ابزار بود. `nslookup` مستقیم از یک DNS عمومی می‌پرسد و رکورد جدید را می‌دید. `Resolve-DnsName` از resolver‌ای می‌پرسد که سیستم‌عامل تنظیم کرده است، و در شبکه خانگی ما این یعنی خود روتر. پس روتر هنوز رکورد قدیمی، یعنی مقدار پیش از تغییر نیم‌سرور، را برمی‌گرداند. مرورگر هم از همین مسیر resolve می‌کرد، پس دقیقاً همان IP قدیمی را می‌دید.

حالا سؤال دوم مانده بود: چرا آن IP قدیمی به سامانه حسابداری می‌رسید؟ آن سرور میزبان سایت دیگری هم بود و دامنه سایت شخصی دیگر در تنظیماتش جایی نداشت. اینجا باید سراغ نحوه انتخاب server block در Nginx می‌رفتیم.

نکات کلیدی

  • ابزارهای مختلف از resolverهای مختلف می‌پرسند؛ مقایسه‌شان خودش یک تست تشخیصی است.
  • فرضیه‌های «CDN خراب است» و «فقط کش مرورگر است» رد شدند.

علت اصلی: دو کش و یک default_server ضمنی

علت اصلی از سه تکه ساخته شده بود که هر کدام به‌تنهایی بی‌ضرر بودند.

**۱) کش DNS.** هر رکورد DNS یک فیلد TTL دارد. بر اساس RFC 1035، این فیلد بازه زمانی‌ای را مشخص می‌کند که رکورد مجاز است کش شود، قبل از آنکه دوباره از منبع اصلی پرسیده شود. روتر خانگی و سیستم‌عامل هر دو می‌توانند جواب را تا پایان TTL نگه دارند. تغییر نیم‌سرور هم همه‌جا یک‌باره اعمال نمی‌شود و هر resolver در لحظه‌ای متفاوت به‌روز می‌شود. پس برای مدتی بعضی دستگاه‌ها هنوز IP قدیمی را می‌گیرند.

**۲) انتخاب server block در Nginx.** طبق مستندات رسمی Nginx، برای درخواست‌های روی یک پورت، فقط هدر `Host` تعیین می‌کند کدام server block جواب بدهد. اگر مقدار `Host` با هیچ `server_name` نخواند، درخواست به سرور پیش‌فرض آن پورت می‌رسد. و اگر `default_server` صریح تعیین نشده باشد، اولین server block تعریف‌شده برای آن پورت پیش‌فرض است. مستندات تأکید می‌کنند سرور پیش‌فرض ویژگی پورت listen است، نه ویژگی نام سرور.

روی سروری که IP قدیمی به آن می‌رسید، دامنه سایت شخصی دیگر هیچ بلوکی نداشت. پس درخواست به همان اولین بلوک پورت ۸۰ می‌افتاد که بدون اعلام صریح، پیش‌فرض شده بود. این بلوک هر درخواست ناشناس را با 301 به دامنه خودش، یعنی سامانه حسابداری، می‌فرستاد.

**۳) ماندگاری 301.** کد 301 یعنی منبع برای همیشه جابه‌جا شده است. طبق مستندات MDN، چنین ریدایرکتی فرض می‌کند آدرس قبلی دیگر نباید استفاده شود. در عمل مرورگرها هم این پاسخ را نگه می‌دارند. RFC 9111 توضیح می‌دهد که کش‌ها می‌توانند برای پاسخ‌های بدون زمان انقضای صریح، فقط وقتی کد وضعیت «قابل کش به‌صورت heuristic» باشد، از heuristic استفاده کنند، و 301 جزو این کدهاست. نتیجه: حتی بعد از اینکه DNS درست شد، مرورگر من درخواست را اصلاً به شبکه نمی‌فرستاد و از همان 301 ذخیره‌شده پیروی می‌کرد.

یعنی دو لایه کش، یکی در DNS و یکی در مرورگر، روی هم یک خطای اولیه را تثبیت کرده بودند.

نکات کلیدی

  • TTL تعیین می‌کند resolverها تا کی مجازند جواب قدیمی را نگه دارند.
  • در Nginx اگر default_server نگذاریم، اولین بلوک روی آن پورت پیش‌فرض می‌شود.
  • 301 در مرورگر ماندگار می‌شود؛ پس خطای یک ریدایرکت اشتباه بعد از رفع علت هم می‌ماند.

راه‌حل

روی سرور قدیمی یک server block مخصوص دامنه قبلی گذاشتیم، تا درخواست‌هایی که هنوز با IP قدیمی می‌رسند به محصول دیگری نروند. سپس کش DNS دستگاه و کش مرورگر را پاک کردیم و آدرس دوباره درست باز شد.

در کنار آن یک `default_server` صریح هم تعریف کردیم که با درخواست ناشناس هرگز ریدایرکت نمی‌کند. نمونه عمومی آن این است:

نکته: برای ریدایرکت‌های موقت از 302 استفاده کنید. 301 را فقط وقتی بزنید که واقعاً مطمئنید جابه‌جایی دائمی است.

nginx
# بلوک پیش‌فرض صریح: هر Host ناشناس را بدون ریدایرکت رد می‌کند
server {
    listen 80 default_server;
    server_name _;
    return 444;   # اتصال بسته می‌شود؛ یا return 404;
}

# بلوک مخصوص دامنه قبلی (تا وقتی IP قدیمی هنوز ترافیک می‌گیرد)
server {
    listen 80;
    server_name old-site.example.com;
    # ریدایرکت موقت؛ تا مطمئن نشده‌ایم، 301 نمی‌زنیم
    return 302 https://new-site.example.com$request_uri;
}

# محصول واقعی روی همین سرور
server {
    listen 80;
    server_name app.example.com;
    # ...
}

درس‌ها و چک‌لیست

این اتفاق خطای پیچیده‌ای نبود. چند پیش‌فرض ساده بود که هر کدام جداگانه معقول به نظر می‌رسید و فقط کنار هم مشکل ساختند. چیزهایی که از این به بعد در مهاجرت دامنه رعایت می‌کنیم:

- چند روز قبل از تغییر نیم‌سرور یا IP، TTL رکوردها را کم کنیم تا کش‌ها سریع‌تر منقضی شوند. - روی هر سرور که ممکن است IP قدیمی به آن برسد، بدانیم چه چیزی جواب می‌دهد. یک بلوک مخصوص دامنه قبلی یا حداقل یک `default_server` صریح داشته باشیم. - `default_server` هرگز به محصول دیگری ریدایرکت نکند. بهتر است درخواست ناشناس را رد کند. - بعد از تغییر، تست را فقط با یک ابزار انجام ندهیم. `nslookup` و resolver سیستم‌عامل (مثلاً `Resolve-DnsName`) و یک شبکه دیگر را مقایسه کنیم. - ریدایرکت‌های موقت را با 302 بزنیم و 301 را برای جابه‌جایی قطعی نگه داریم. - اگر خطای عجیبی دیدیم، هم کش DNS و هم کش مرورگر را پاک کنیم و بعد نتیجه را بخوانیم.

نکات کلیدی

  • TTL را قبل از مهاجرت کم کنید، نه بعد از آن.
  • default_server صریح داشته باشید و آن را به هیچ محصول دیگری ریدایرکت نکنید.
  • با چند resolver تست کنید، نه فقط یک ابزار.
  • برای ریدایرکت موقت 302 بزنید.

جمع‌بندی

نیم‌سرور درست عوض شده بود و CDN هم سالم بود. مشکل جایی بود که انتظارش را نداشتیم: یک resolver خانگی که رکورد قدیمی را نگه داشته بود، یک server block که ضمنی پیش‌فرض شده بود و یک 301 که مرورگر آن را برای همیشه به خاطر سپرد. بعد از این تجربه، در هر مهاجرت TTL را از قبل کم می‌کنیم، `default_server` را صریح می‌نویسیم و برای هر ریدایرکتی که قطعی نیست از 302 استفاده می‌کنیم.

منابع

  1. How nginx processes a requestnginx.org
  2. RFC 1035: Domain Names - Implementation and Specificationrfc-editor.org
  3. RFC 9111: HTTP Cachingrfc-editor.org
  4. MDN: HTTP 301 Moved Permanentlydeveloper.mozilla.org