الخلاصة

  • close_notify في TLS 1.3 تصريح مصادق عليه لاتجاه واحد: لن يرسل صاحبه رسائل TLS أخرى على الاتصال. لا يغلق اتجاه الطرف المقابل، ولا يقر بطلب أو استجابة أو stream أو أثر دائم.
  • يحافظ EOF في النقل من دون التنبيه على احتمال البتر. لا يجوز اعتباره نهاية مقبولة إلا إذا كان بروتوكول التطبيق يحدد اكتمال الرسالة مستقلاً، وأثبت التطبيق أنه نفذ هذا الفحص.
  • تتطلب سلطة إعادة المحاولة أربعة حدود محفوظة منفصلة: نهاية رسالة التطبيق، وإغلاق TLS المحلي والبعيد، ونهاية النقل، والنتيجة الدائمة. حقل واحد باسم “closed” يمحو هذا الدليل.

سجل صحيح لسؤال خاطئ

أنشأ الخادم استجابة النجاح قبل التثبيت النهائي. كتب سجلات TLS وأرسل close_notify، فسجل الغلاف shutdown=success. بعد ذلك رفض قيد فريد تحديث قاعدة البيانات.

كان التسلسل على الشبكة صحيحاً. سبقت البيانات التي اختارها الخادم تنبيه النهاية المصادق عليه. لكن العميل لم يتلق معرّف تثبيت من النظام المسؤول عن الحقيقة التجارية.

المشكلة أن آخر حدث في الاتصال أصبح حكماً على نظام آخر. يستطيع TLS ترتيب البايتات وحمايتها، لكنه لا يرى معاملة قاعدة البيانات أو حالة الطابور أو قرار الخدمة اللاحقة.

لكل اتجاه نهاية مستقلة

يعلن مرسل close_notify أنه لن يرسل رسائل TLS أخرى، ويجب تجاهل البيانات اللاحقة للتنبيه. ينهي ذلك جانب الكتابة لديه، بينما يمكن أن يواصل القراءة لأن الطرف الآخر قد يملك بيانات أخيرة.

على كل طرف إرسال التنبيه قبل إغلاق جانب الكتابة، ما لم يكن قد أرسل تنبيه خطأ. أزال TLS 1.3 وجوب الرد الفوري الذي كان قد يتخلص من كتابات معلقة. لذلك لا يوجد حدث إغلاق ذري واحد للاتصال كله.

يوفر TCP إغلاقاً نصفياً أيضاً، لكن FIN دليل على نهاية تسلسل بايتات، لا تنبيه TLS مصادقاً عليه. ولا يحدد آخر سجل TLS مكتمل أو معنى رسالة التطبيق.

EOF يترك الاحتمال مفتوحاً

إذا انتهى النقل قبل close_notify فلا يعرف المستقبل هل وصل كل ما أراد المرسل إرساله. قد يتعطل process، أو ينتهي proxy بمهلة، أو يغلق تنفيذ قديم TCP فقط. الغياب لا يثبت هجوماً، لكنه يعني أن الحد التشفيري الأقوى مفقود.

يجب فصل التنبيه القاتل وRST والمهلة وEOF غير المتوقع وتنبيه الطرف المستلم ومحاولة الإرسال المحلية. جمعها تحت “disconnected” يحذف الاتجاه والترتيب اللازمين للحكم.

تسند IANA القيمة 0 إلى close_notify. يثبت السجل تفسير codepoint، لا أن اتصال إنتاج بعينه أرسله أو استلمه، ولا أن الرسالة السابقة كانت كاملة.

التطبيق هو من يعرّف اكتمال الرسالة

يتعامل TLS مع محتوى السجلات بوصفه بايتات مبهمة. وصولها قبل التنبيه يثبت الترتيب، ولا يثبت أنها وثيقة كاملة أو chunk أخير أو إيصالاً أو تثبيتاً.

يحتاج البروتوكول الأعلى إلى حد خاص به: طول معلن، delimiter، chunk نهائي، FIN للـstream، رمز نهائي، إقرار، معرّف طلب أو سجل دائم. اكتمال الرسالة ونجاح العملية حقيقتان منفصلتان؛ قد توجد إحداهما دون الأخرى.

تضيف buffers حالات وسطية. نجاح دالة الكتابة قد يعني دخول البايتات إلى TLS أو BIO فقط. وفي I/O غير الحاجب قد تسجل محاولة التنبيه قبل أن تصل فعلاً إلى النقل. لا يثبت أي منهما دوام أثر خارجي.

أرقام OpenSSL حالات وليست حكماً واحداً

تعني عودة SSL_shutdown() بالقيمة 0 عادة أن close_notify المحلي أرسل وأن تنبيه الطرف لم يصل بعد. ليست خطأ، وليست إغلاقاً ثنائي الاتجاه. تعني القيمة 1 إرسال التنبيهين واستلامهما.

يغلق الاستدعاء الأول كتابة TLS، ويبقي القراءة ممكنة وTCP مفتوحاً. توصي OpenSSL بمتابعة القراءة لمعالجة البيانات الأخيرة ورسائل ما بعد المصافحة. قد يفشل الاستدعاء التالي إذا بقيت بيانات تطبيق غير مقروءة.

يتبع flag الإرسال محاولة التنبيه، ويتبع flag الاستلام تنبيه الطرف. يغير quiet shutdown الحالة محلياً من دون إرسال تنبيه، وهو غير مطابق. منذ OpenSSL 3.0 أصبح EOF غير المتوقع خطأ TLS ذا معنى. لا يصلح SSL_OP_IGNORE_UNEXPECTED_EOF إلا عندما يكشف بروتوكول التطبيق البتر بلا لبس وينفذ الفحص.

يفصل GnuTLS بين GNUTLS_SHUT_WR وGNUTLS_SHUT_RDWR، ويتابع BoringSSL حالتي القراءة والكتابة كلتيهما. توفر المكتبات الفرق؛ على القياس ألا يطمسه.

يملك HTTP قاعدة الاكتمال الناقصة

يتطلب Content-Length في HTTP/1.1 كل البايتات المعلنة، ويتطلب chunked الـchunk الصفري النهائي. لا يجعل إغلاق الاتصال جسماً ناقصاً كاملاً.

الاستجابة المحددة بالإغلاق وحده أضعف. تعتمد نهايتها على نهاية اتصال صحيحة؛ لذا يجعل إغلاق TLS غير المكتمل الرسالة غير مكتملة. يفضل RFC 9112 الطول أو الترميز الصريح لأن عطل الشبكة قد يبدو كنهاية عادية.

إذا تحقق التطبيق من الطول أو الـchunk النهائي قبل غياب تنبيه الطرف، يبقى حد الرسالة دليلاً صالحاً. هذا هو شرط تحمل EOF: إثبات مستقل لكل رسالة سيُعمل بها، لا مجرد تشغيل خيار توافق.

تعدد الـstreams يحتاج حد الطلب

يحمل HTTP/2 عدة streams فوق اتصال TLS واحد. لا يحدد close_notify الطلبات التي بدأ الخادم معالجتها. يضيف GOAWAY آخر stream ليحد ما قد يكون عولج.

من دون GOAWAY يبقى POST غير idempotent في أثناء النقل غامضاً. يستخدم HTTP/3 الفكرة نفسها فوق QUIC ويحدد بالـGOAWAY الطلبات المقبولة. لا ينشئ إغلاق الاتصال إقراراً لكل طلب.

قد تكرر إعادة كل شيء الدفع مرتين، وقد يضيع العمل إن لم نكرر شيئاً. تأتي سلطة الإعادة من معنى method ومفتاح idempotency والاستعلام عن النتيجة والمطابقة.

ينهي QUIC الاتصال بآليات أخرى

يستخدم QUIC TLS للمصافحة لكنه لا يحمي بيانات التطبيق بسجلات TLS. تتحول تنبيهات TLS إلى أخطاء اتصال QUIC، ولـQUIC حالات CONNECTION_CLOSE وFIN للـstream وreset وclosing وdraining. لا تنتقل دلالة warning الخاصة بـclose_notify إليه.

يحتاج HTTP/3 إلى GOAWAY لأن إغلاق QUIC وحده لا يحدد الطلبات المقبولة. ينبغي للمقاييس تسمية الدليل: تنبيه TLS، أو TCP FIN/RST، أو QUIC stream FIN، أو CONNECTION_CLOSE، أو idle timeout، أو HTTP GOAWAY، أو إقرار التطبيق.

سجل أدلة للنهايات

احفظ دور endpoint والاتجاه وإصدار TLS ومعرّف الربط وآخر رسالة أو stream مكتمل. سجل قاعدة framing وعدد البايتات المتوقع والمستلم ومعرّف الطلب ومفتاح idempotency وفئة الإعادة.

في TLS افصل التنبيه المحلي المدرج والمرسل وتنبيه الطرف المستلم، وتسلسل عوائد المكتبة، وحالتي القراءة والكتابة، والبيانات المعلقة، وتصنيف الخطأ. احفظ quiet mode وسياسة EOF وإصدار المكتبة وkTLS وحدود الـproxy.

في النقل ميّز FIN وRST وEOF والمهلة والإغلاق النصفي. في التطبيق احفظ الإقرار ومعرّف التثبيت ووقته الدائم والسلطة التي أصدرته. وفي البروتوكولات المتعددة احفظ GOAWAY ونهاية كل stream.

ينبغي للاختبارات السلبية قطع TCP قبل التنبيه، وحذف chunk النهاية، والإغلاق بعد frame كامل وقبل التثبيت، ورصد عودة 0، وترك بيانات الطرف معلقة، وتشغيل quiet shutdown، وتبديل سياسة EOF، وإنهاء HTTP/2 أو HTTP/3 مع طلب غير idempotent جارٍ.

المصادر