সরাসরি পর্বে যান
পর্ব ২৯ · ৬২-এর মধ্যে · মডিউল ৫

TCP সংযোগ — three-way handshake, state আর বন্ধ করা

TCP Connection Management — Header, Three-Way Handshake, State Machine, FIN/RST Teardown, TIME-WAIT, Listen Backlog & SYN Cookies

Taught in Bangla, this lesson covers TCP connection management: the header fields, the three-way handshake and random initial sequence numbers, the TCP state machine, four-message FIN teardown, why TIME-WAIT lasts 2·MSL, RST and connection refused, the SYN and accept queues of the listen backlog, SYN floods and SYN cookies, with live C programs.

এই পর্বে যা শিখবেন

  • TCP header-এর প্রতিটি ঘর — hex বাইট থেকে একটি আসল SYN নিজে পড়তে পারা
  • Three-way handshake কেন তিন ধাপের, ISN কেন এলোমেলো, আর seq/ack হিসাব পরীক্ষার মতো করে হাতে করা
  • ১১টি TCP state, client ও server-এর পথ, simultaneous close আর RST
  • FIN, half-close, TIME-WAIT আর 2·MSL — ব্যস্ত সার্ভারে TIME-WAIT সমস্যার ঠিক আর ভুল সমাধান
  • Listen backlog, SYN flood-এর প্রতিরক্ষা, SYN cookie, keepalive আর TCP Fast Open

১ · TCP কী সেবা দেয় — দুই প্রান্তের মাঝে একটি নির্ভরযোগ্য নল

পর্ব ২৭-এ দেখেছেন, transport স্তর port নম্বর দিয়ে একটি কম্পিউটারের ভেতরের নির্দিষ্ট প্রোগ্রামের কাছে ডেটা পৌঁছায়। পর্ব ২৮-এর UDP সেখানে থেমে যায় — একটি datagram পাঠায়, পৌঁছাল কি না খোঁজ রাখে না। কিন্তু ব্যাংকের লেনদেন, ওয়েবপেজ, ইমেইল বা ফাইল ডাউনলোডে একটি বাইটও হারানো চলে না, ক্রমও উল্টানো চলে না। এই কাজের জন্য আছে TCP.

নিচের IP স্তর কোনো নিশ্চয়তা দেয় না: packet হারাতে পারে, দুবার পৌঁছাতে পারে, ক্রম উল্টে যেতে পারে। TCP এই অবিশ্বস্ত ভিত্তির ওপর দাঁড়িয়ে প্রোগ্রামকে একটি পরিষ্কার প্রতিশ্রুতি দেয়। TCP-র সেবার পাঁচটি বৈশিষ্ট্য মনে রাখুন:

সংযোগভিত্তিক

ডেটা পাঠানোর আগে দুই প্রান্ত handshake করে একটি সংযোগ বানায়, আর দুজনেই তার অবস্থা (state) মনে রাখে। শেষে সংযোগ নিয়ম মেনে বন্ধ হয়। এই পর্বের মূল বিষয় এটাই।

শুরু, চলা, শেষ

নির্ভরযোগ্য ও ক্রম-ঠিক

প্রতিটি বাইটের একটি নম্বর আছে। হারালে আবার পাঠানো হয়, নকল এলে বাদ যায়, উল্টো ক্রমে এলে সাজিয়ে দেওয়া হয়। এর বিস্তারিত পর্ব ৩০-এ।

seq, ACK, retransmission

Byte stream

TCP বার্তার সীমানা রাখে না। প্রেরক ১০০ বাইট আর ২০০ বাইট দুবারে লিখলেও প্রাপক একবারে ৩০০ পড়তে পারে, বা ৫০ করে ছয়বার। বার্তার সীমানা application-কেই ঠিক করতে হয়।

সীমানাহীন বাইট

Full-duplex, ঠিক দুজন

একটি সংযোগে দুই দিকে একসাথে ডেটা চলে (full-duplex). সংযোগ সবসময় ঠিক দুটি প্রান্তের মধ্যে — একজন থেকে অনেকজনকে (multicast বা broadcast) TCP পাঠাতে পারে না।

point-to-point

এর সাথে আরও দুটি সেবা আছে, যা পরের পর্বগুলোর বিষয়: flow control (ধীর প্রাপককে ডুবিয়ে না দেওয়া, পর্ব ৩১) আর congestion control (নেটওয়ার্কের জ্যাম বুঝে গতি কমানো, পর্ব ৩২)।

কুরিয়ার সার্ভিসের কথা ভাবুন। UDP হলো ডাকবাক্সে চিঠি ফেলে দেওয়া — পৌঁছাল কি না জানার উপায় নেই। TCP হলো এমন একটি কুরিয়ার চুক্তি, যেখানে পাঠানোর আগে দুই অফিস ফোনে নিশ্চিত হয় দুজনেই প্রস্তুত (handshake), প্রতিটি পার্সেলে ট্র্যাকিং নম্বর থাকে (sequence number), প্রাপক প্রতিবার জানায় “অমুক নম্বর পর্যন্ত পেয়েছি” (ACK), আর কাজ শেষে দুই পক্ষই আলাদা করে খাতা বন্ধ করে (FIN). পার্থক্য একটাই: TCP পার্সেল গোনে না, গোনে প্রতিটি বাইট।

একটি TCP সংযোগকে চেনা যায় চারটি মান দিয়ে: উৎস IP, উৎস port, গন্তব্য IP আর গন্তব্য port (protocol ধরলে 5-tuple). একটি ওয়েব সার্ভার port 443-এ হাজার হাজার client-এর সাথে একসাথে কথা বলে, কারণ প্রতিটি সংযোগের চার-মানের জোড়া আলাদা। কীভাবে OS আসা segment-কে ঠিক socket-এ পাঠায় (demultiplexing), তা পর্ব ২৭-এ দেখেছেন।

মূল ধারণা

TCP-র “সংযোগ” তারের ভেতরে কোনো বাস্তব পথ নয় — circuit switching-এর মতো কিছু সংরক্ষিত হয় না। সংযোগ আসলে দুই প্রান্তের kernel-এ রাখা কিছু হিসাব: কোন বাইট পাঠানো হয়েছে, কোনটি পৌঁছেছে, অন্য পক্ষ কত নিতে পারবে, আর সংযোগটি এখন কোন অবস্থায়। পথের router-রা এর কিছুই জানে না (NAT বা firewall-এর মতো middlebox বাদে)। তাই সংযোগ তৈরি মানে এই হিসাবের খাতা দুই দিকে খোলা, আর বন্ধ মানে নিরাপদে খাতা বন্ধ করা।

২ · TCP header — প্রতিটি ঘর, আর একটি আসল SYN পড়া

প্রতিটি TCP segment-এর সামনে থাকে কমপক্ষে ২০ বাইটের header. RFC-এর পরিচিত ছবিতে প্রতিটি সারি ৩২ bit (৪ বাইট):

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
┌───────────────────────────────┬───────────────────────────────┐
│          Source Port          │       Destination Port        │
├───────────────────────────────┴───────────────────────────────┤
│                        Sequence Number                        │
├───────────────────────────────────────────────────────────────┤
│                     Acknowledgment Number                     │
├───────┬───────┬─┬─┬─┬─┬─┬─┬─┬─┬───────────────────────────────┤
│ Data  │ Rsrvd │C│E│U│A│P│R│S│F│                               │
│Offset │       │W│C│R│C│S│S│Y│I│            Window             │
│       │       │R│E│G│K│H│T│N│N│                               │
├───────┴───────┴─┴─┴─┴─┴─┴─┴─┴─┼───────────────────────────────┤
│           Checksum            │        Urgent Pointer         │
├───────────────────────────────┴───────────────────────────────┤
│              Options (0–40 বাইট) … + padding                  │
├───────────────────────────────────────────────────────────────┤
│                     ডেটা (payload)                            │
└───────────────────────────────────────────────────────────────┘
ঘরমাপকাজ
Source / Destination Port১৬ + ১৬ bitকোন প্রোগ্রাম থেকে কোন প্রোগ্রামে (port); IP ঠিকানার সাথে মিলে সংযোগ চেনায়।
Sequence Number৩২ bitএই segment-এর প্রথম ডেটা বাইটের নম্বর (TCP sequence number). SYN-এ এটিই ISN.
Acknowledgment Number৩২ bitপ্রাপক পরের যে বাইট চায় তার নম্বর (acknowledgment number). শুধু ACK flag চালু থাকলে অর্থবহ।
Data Offset৪ bitHeader কত লম্বা, ৪ বাইটের এককে (data offset). ৫ = ২০ বাইট, সর্বোচ্চ ১৫ = ৬০ বাইট।
Reserved৪ bitভবিষ্যতের জন্য, শূন্য থাকে।
Flags৮ bitCWR, ECE, URG, ACK, PSH, RST, SYN, FIN (TCP flags) — নিচের তালিকা দেখুন।
Window১৬ bitপ্রাপক আর কত বাইট নিতে প্রস্তুত (receive window); flow control-এর ভিত্তি, পর্ব ৩১।
Checksum১৬ bitHeader, ডেটা আর একটি pseudo-header (উৎস/গন্তব্য IP, protocol, দৈর্ঘ্য) মিলিয়ে হিসাব করা checksum.
Urgent Pointer১৬ bitURG চালু থাকলে জরুরি ডেটার শেষ কোথায় (urgent pointer). প্রায় অব্যবহৃত।
Options০–৪০ বাইটMSS, window scale, SACK-permitted, timestamp ইত্যাদি (TCP options).

আটটি flag — প্রতিটি একটি করে bit

Flagbit-এর মানঅর্থ
CWR0x80Congestion Window Reduced — প্রেরক জানায় যে জ্যামের সংকেত পেয়ে সে গতি কমিয়েছে (ECN, RFC 3168).
ECE0x40ECN-Echo — পথে জ্যামের দাগ পাওয়া গেছে, প্রেরককে জানানো। SYN-এ ECE+CWR মানে “আমি ECN বুঝি”।
URG0x20Urgent pointer ঘরটি অর্থবহ।
ACK0x10Acknowledgment number অর্থবহ। প্রথম SYN ছাড়া প্রায় সব segment-এ চালু থাকে।
PSH0x08Push — ডেটা জমিয়ে না রেখে দ্রুত প্রোগ্রামের হাতে দেওয়ার ইঙ্গিত।
RST0x04Reset — সংযোগ এখনই বাতিল (RST).
SYN0x02Synchronize — সংযোগ শুরু, ISN জানানো (SYN).
FIN0x01Finish — আমার দিকের ডেটা শেষ (FIN).

ছোট একটি হিসাব পরীক্ষায় প্রায়ই আসে: data offset ৪ bit, তাই সর্বোচ্চ মান ১৫, header সর্বোচ্চ ১৫ × ৪ = ৬০ বাইট; নির্দিষ্ট অংশ ২০ বাইট, তাই option-এর জায়গা সর্বোচ্চ ৪০ বাইট। Window ঘর ১৬ bit, তাই scaling ছাড়া সর্বোচ্চ window ৬৫,৫৩৫ বাইট।

একটি আসল SYN, বাইট ধরে ধরে

পর্ব ১৬-এ একটি container (10.0.2.100) থেকে ওয়েব সার্ভারে (192.0.2.80) যাওয়া প্রথম packet-এর IPv4 header পড়েছিলেন। তার Total Length ছিল ৬০ বাইট — ২০ বাইট IP, বাকি ৪০ বাইট TCP. এবার সেই ৪০ বাইট খুলে দেখা যাক। Linux-এর একটি সাধারণ SYN দেখতে এমন (প্রতি ঘর ৪ hex অঙ্ক = ২ বাইট):

b26e 01bb 9a3f 21c4 0000 0000 a002 faf0 6a36 0000
0204 05b4 0402 080a 0016 a3e1 0000 0000 0103 0307
যে বাইটগুলো পড়ছি আর তার অর্থ
বাইট
ঘর
মান
মোট পড়া
  1. Port. 0xb26e = ৪৫৬৭৮ — Linux-এর ephemeral port পরিসরের (32768–60999) একটি অস্থায়ী port, OS নিজে বেছেছে। 0x01bb = ৪৪৩ — HTTPS. এই segment browser-জাতীয় কোনো client থেকে ওয়েব সার্ভারে যাচ্ছে।
  2. Sequence আর acknowledgment. 0x9a3f21c4 = 2587828676. এটি client-এর ISN — একটি এলোমেলো-দেখতে সংখ্যা, শূন্য নয়। Acknowledgment ঘর শূন্য, কারণ ACK flag বন্ধ; client এখনো সার্ভারের কোনো নম্বর জানেই না, তাই স্বীকার করার কিছু নেই।
  3. Offset আর flag. 0xa0-এর ওপরের ৪ bit a = ১০, তাই header ১০ × ৪ = ৪০ বাইট — অর্থাৎ ২০ বাইট option আছে। পরের বাইট 0x02 = binary 00000010: শুধু SYN bit চালু। এটাই handshake-এর প্রথম বার্তা।
  4. Window, checksum, urgent. 0xfaf0 = ৬৪২৪০ = ৪৪ × ১৪৬০, অর্থাৎ ৪৪টি পূর্ণ segment-এর জায়গা। SYN-এর window কখনো scale হয় না, তাই এটি আক্ষরিক মান। Checksum 0x6a36 pseudo-header (10.0.2.100, 192.0.2.80, protocol 6, দৈর্ঘ্য ৪০) সহ হিসাব করা — যাচাই করলে মোট যোগফল আসে 0xffff. URG বন্ধ, তাই urgent pointer শূন্য।
  5. Option — দর-কষাকষির প্রস্তাব। প্রতিটি option শুরু হয় kind বাইট দিয়ে, তারপর দৈর্ঘ্য। 02 04 05b4: MSS = ১৪৬০। 04 02: “SACK চালাতে পারি”। 08 0a …: timestamp, TSval 1483745 আর TSecr 0 (এখনো প্রতিধ্বনি করার কিছু নেই)। 01: NOP, শুধু সারিবদ্ধ করার ফাঁকা বাইট। 03 03 07: window scale, shift ৭, অর্থাৎ পরে window ঘরের মানকে ২৭ = ১২৮ দিয়ে গুণ করতে হবে।

এই চারটি option-এর প্রতিটি একটি সমস্যার সমাধান। MSS বলে “আমাকে এক segment-এ এর বেশি ডেটা পাঠাবেন না” — হিসাবটা সহজ: Ethernet-এর MTU ১৫০০ থেকে IPv4 header ২০ আর TCP header ২০ বাদ দিলে ১৪৬০ (IPv6-এ ৪০ বাইট header, তাই ১৪৪০)। খেয়াল করুন, MSS-এ option-এর বাইট ধরা হয় না; timestamp চালু থাকলে প্রতিটি segment-এ আসল ডেটা আরও ১২ বাইট কম যায়। Window scale দ্রুত, লম্বা পথের জন্য ৬৫,৫৩৫-এর সীমা ভাঙে। SACK হারানো খণ্ড ঠিকঠাক জানাতে দেয়, আর timestamp RTT মাপে আর পুরোনো segment চিনতে সাহায্য করে। এদের প্রায় সবই শুধু SYN-এ প্রস্তাব করা যায় — দুই পক্ষই প্রস্তাব দিলে তবেই চালু হয়। এই কারণে handshake শুধু “হ্যালো” নয়, একটি চুক্তিও।

৩ · Three-way handshake — কেন ঠিক তিনটি বার্তা

Three-way handshake-এর কাজ একটাই: দুই প্রান্তকে একে অপরের প্রথম sequence number-এ একমত করা, আর দুজনেই যেন জানে যে অন্যজন সেটা পেয়েছে। নিচের ডায়াগ্রামে client-এর ISN ১০০০, server-এর ৫০০০ ধরা হয়েছে (বাস্তবে এগুলো এলোমেলো বড় সংখ্যা, পরীক্ষায় হিসাব সহজ রাখতে ছোট নেওয়া হয়)। “পরে” চাপুন:

Client 10.0.2.100:45678 ⇄ Server 192.0.2.80:443
তারে যা গেল
Client-এর অবস্থা
Server-এর অবস্থা
কে কী নিশ্চিত জানে
  1. প্রস্তুতি। Server প্রোগ্রাম socket(), bind(), listen() ডেকে port 443-এ LISTEN অবস্থায় বসে আছে — অফিসের রিসেপশনে কেউ ফোনের পাশে অপেক্ষা করছেন। Client এখনো CLOSED.
  2. SYN. Client-এর প্রোগ্রাম connect() ডাকল। Kernel একটি এলোমেলো ISN বাছল (এখানে ১০০০) আর SYN পাঠাল, সাথে নিজের option-এর প্রস্তাব। SYN-এ কোনো ডেটা নেই, তবু সে একটি sequence number খরচ করে — ১০০০ নম্বরটি SYN নিজেই নিল।
  3. SYN+ACK. Server নিজের ISN (৫০০০) বাছল আর একটি segment-এ দুটি কাজ করল: SYN দিয়ে নিজের নম্বর জানাল, ACK দিয়ে বলল “১০০১ চাই” — অর্থাৎ ১০০০ (client-এর SYN) পেয়েছি। Option-এ সেও নিজের MSS আর সক্ষমতা জানাল; দুজনের প্রস্তাব মিলেই চূড়ান্ত হয়।
  4. ACK. Client বলল “৫০০১ চাই” — server-এর SYN পৌঁছেছে। Client SYN+ACK পেয়েই ESTABLISHED হয়ে গেছে; server এই ACK পেলে ESTABLISHED হয়। এই ACK-এ ডেটা না থাকায় seq ১০০১ খরচ হয় না।
  5. প্রথম ডেটা। Client-এর প্রথম ডেটা বাইটের নম্বর ১০০১, ১০০ বাইট পাঠালে পরের বাইট ১১০১। খেয়াল করুন, তৃতীয় ACK আর এই ডেটার seq একই (১০০১) — কারণ খালি ACK নম্বর খরচ করে না। বাস্তবে অনেক সময় তৃতীয় ACK-এর সাথেই ডেটা চলে যায়।

দুটি বার্তায় কেন হয় না

ধরুন handshake দুই ধাপের হতো: client SYN পাঠায়, server SYN+ACK দিয়েই ESTABLISHED. তাহলে server জানতেই পারত না তার নিজের ISN client-এর কাছে পৌঁছেছে কি না। আরও বড় বিপদ — পুরোনো নকল SYN. আগের কোনো সংযোগের একটি SYN নেটওয়ার্কে কোথাও আটকে ছিল, অনেক পরে server-এ পৌঁছাল। দুই ধাপের নিয়মে server সেটাকে নতুন সংযোগ ভেবে ESTABLISHED হয়ে বসে থাকত, এমনকি ডেটাও পাঠাতে শুরু করত — অথচ client-এর কোনো ধারণাই নেই।

তিন ধাপে এটি হয় না। Server-এর SYN+ACK পেয়ে client দেখে, ack নম্বরটি তার বর্তমান কোনো SYN-এর সাথে মেলে না। তখন সে ACK না দিয়ে RST পাঠায়, আর server সারিটি মুছে ফেলে। মূল নিয়মটি হলো: প্রতিটি দিকের ISN একবার পাঠাতে হয় আর একবার স্বীকার করাতে হয় — দুই দিক × দুই কাজ = চারটি কাজ। মাঝের দুটি (server-এর ACK আর server-এর SYN) একটি segment-এ জোড়া যায়, তাই তিনটি বার্তা।

১৯৭৫ সালে Ray Tomlinson (ইমেইলে @ চিহ্ন ব্যবহারের জন্য যিনি বিখ্যাত) sequence number বাছাই নিয়ে একটি গবেষণাপত্রে three-way handshake-এর ধারণা দেন। ১৯৭৪ সালে Vint Cerf আর Bob Kahn TCP-র প্রথম নকশা প্রকাশ করেছিলেন; ১৯৮১ সালে RFC 793 TCP-কে আনুষ্ঠানিক রূপ দেয়, যার state diagram আজও পরীক্ষার খাতায় আঁকা হয়। ২০২২ সালে RFC 9293 পুরোনো RFC 793 আর তার বহু সংশোধন এক জায়গায় এনে আধুনিক মানদণ্ড হয়েছে।

ISN কেন এলোমেলো

প্রথম কারণ নির্ভরযোগ্যতা: একই চার-মানের (IP আর port) জোড়ায় পুরোনো সংযোগ বন্ধ হয়ে নতুন সংযোগ খুললে, পুরোনো সংযোগের দেরিতে আসা segment-এর নম্বর যেন নতুন সংযোগের বৈধ পরিসরে না পড়ে। RFC 793 এজন্য প্রায় প্রতি ৪ মাইক্রোসেকেন্ডে এক করে বাড়া একটি ঘড়ি থেকে ISN নেওয়ার কথা বলেছিল।

দ্বিতীয় কারণ নিরাপত্তা, যা পরে বোঝা গেছে। ISN অনুমান করা গেলে একজন আক্রমণকারী অন্যের IP ঠিকানা নকল করে (IP spoofing) উত্তর না দেখেও একটি TCP সংযোগ “জাল” করতে পারত, কারণ handshake-এর তৃতীয় ACK-এ কোন নম্বর বসাতে হবে তা সে আগেই জানত। তাই আধুনিক নিয়ম (RFC 6528, ২০১২): ISN = একটি ঘড়ি + চার-মানের জোড়া আর একটি গোপন চাবির cryptographic hash. এতে প্রতিটি জোড়ার নম্বর বাইরে থেকে অনুমান-অযোগ্য, অথচ একই জোড়ায় সময়ের সাথে বাড়তেই থাকে। Linux, Windows, macOS-সহ প্রধান OS-গুলো এই ধরনের কৌশল ব্যবহার করে বলে জানা যায়, যদিও প্রত্যেকের সূক্ষ্ম বিবরণ আলাদা।

Simultaneous open — বিরল, কিন্তু বৈধ

দুই প্রান্ত যদি ঠিক একই সময়ে একে অপরের পরিচিত port-এ connect() করে, দুটি SYN পথে একে অপরকে অতিক্রম করে। তখন দুজনেই SYN-SENT থেকে অন্যের SYN পেয়ে SYN-RECEIVED হয়, SYN+ACK পাঠায়, আর সেটি পেয়ে ESTABLISHED — মোট চারটি segment, কোনো LISTEN ছাড়াই। এটি simultaneous open. সাধারণ client-server জীবনে প্রায় দেখা যায় না, তবে দুই NAT-এর পেছনের যন্ত্র TCP hole punching করলে (পর্ব ১৯-এর ধারণা) এই পথই কাজে লাগে।

Handshake-এর দাম: প্রথম ডেটা বাইট পাঠানোর আগেই একটি পূর্ণ RTT চলে যায় (SYN যাওয়া আর SYN+ACK ফেরা)। ঢাকা থেকে দূরের কোনো সার্ভারে RTT ধরা যাক ১৫০ মিলিসেকেন্ড — তাহলে শুধু সংযোগ তৈরিতেই ১৫০ মিলিসেকেন্ড। HTTPS হলে এর ওপর TLS handshake (TLS 1.3-এ আরও এক RTT; বিস্তারিত পর্ব ৪৮)। এই খরচ কমাতেই QUIC (পর্ব ৩৩) transport আর encryption-এর handshake একসাথে করে, আর CDN সার্ভারকে ব্যবহারকারীর কাছে আনে (পর্ব ৩৮)।

৪ · TCP-র state machine — এগারোটি অবস্থা

প্রতিটি TCP সংযোগ জীবনভর কয়েকটি নির্দিষ্ট অবস্থার মধ্যে ঘোরে। কোন অবস্থায় কোন ঘটনা (প্রোগ্রামের ডাক বা আসা segment) এলে কোথায় যেতে হবে, তা RFC-তে নির্দিষ্ট — একে বলে TCP state machine. প্রথমে এগারোটি অবস্থা:

অবস্থামানেকীভাবে বের হয়
CLOSEDকোনো সংযোগ নেই (কাল্পনিক শুরু ও শেষ)।listen() → LISTEN; connect() → SYN পাঠিয়ে SYN-SENT.
LISTENServer নতুন SYN-এর অপেক্ষায়।SYN এলে SYN+ACK পাঠিয়ে (নতুন সংযোগের জন্য) SYN-RECEIVED.
SYN-SENTClient SYN পাঠিয়ে উত্তরের অপেক্ষায়।SYN+ACK → ACK দিয়ে ESTABLISHED; শুধু SYN → SYN-RECEIVED; RST → CLOSED.
SYN-RECEIVEDSYN পেয়ে SYN+ACK পাঠানো হয়েছে।ACK → ESTABLISHED; RST → (server হলে) LISTEN-এ ফেরে।
ESTABLISHEDডেটা আদান-প্রদানের স্বাভাবিক অবস্থা।close() → FIN পাঠিয়ে FIN-WAIT-1; FIN এলে → ACK দিয়ে CLOSE-WAIT.
FIN-WAIT-1নিজের FIN পাঠানো, ACK আসেনি।ACK → FIN-WAIT-2; FIN → ACK দিয়ে CLOSING; FIN+ACK একসাথে → TIME-WAIT.
FIN-WAIT-2নিজের দিক বন্ধ, অন্যজনের FIN-এর অপেক্ষা।FIN → ACK দিয়ে TIME-WAIT.
CLOSE-WAITঅন্যজন বন্ধ করেছে; নিজের প্রোগ্রাম close() ডাকেনি এখনো (CLOSE-WAIT).close() → FIN পাঠিয়ে LAST-ACK.
CLOSINGদুজনেই একসাথে FIN পাঠিয়েছে; নিজের FIN-এর ACK বাকি।ACK → TIME-WAIT.
LAST-ACKদেরিতে বন্ধকারী নিজের FIN পাঠিয়ে শেষ ACK-এর অপেক্ষায়।ACK → CLOSED.
TIME-WAITআগে বন্ধকারী শেষ ACK দিয়ে অপেক্ষায়।2·MSL সময় পরে → CLOSED.

পরীক্ষায় সাধারণত পুরো জীবনচক্রটি পাশাপাশি দুই পথে আঁকতে বলা হয় — client-এর “active” পথ আর server-এর “passive” পথ। সময় ওপর থেকে নিচে চলছে:

   Client (active open, active close)            Server (passive open)
   ──────────────────────────────────            ─────────────────────
   CLOSED                                        CLOSED
                                                   │ listen()
                                                 LISTEN
   connect() ─── SYN seq=x ──────────────────────▶ │
   SYN-SENT                                      SYN-RECEIVED
       │ ◀──────────── SYN+ACK seq=y ack=x+1 ─────┘
   ESTABLISHED ── ACK ack=y+1 ───────────────────▶ ESTABLISHED
       ⋮               (ডেটা দুই দিকে)                 ⋮
   close() ────── FIN seq=u ─────────────────────▶ │
   FIN-WAIT-1                                    CLOSE-WAIT
       │ ◀──────────── ACK ack=u+1 ───────────────┘   (server চাইলে আরও ডেটা পাঠায়)
   FIN-WAIT-2                                        │ close()
       │ ◀──────────── FIN seq=v ack=u+1 ─────────┘
   TIME-WAIT ──── ACK ack=v+1 ───────────────────▶ LAST-ACK → CLOSED
       │ (2·MSL অপেক্ষা)
   CLOSED

বাসের রুটের কথা ভাবুন। বাস নির্দিষ্ট স্টপেজ ছাড়া কোথাও থামে না, আর একটি স্টপেজ থেকে শুধু নির্ধারিত পরের স্টপেজে যায়। TCP সংযোগও তাই: ESTABLISHED থেকে সরাসরি TIME-WAIT-এ যাওয়ার কোনো রাস্তা নেই — যেতে হবে FIN-WAIT-এর স্টপেজ পার হয়ে। পরীক্ষায় state diagram মুখস্থ না করে প্রতিটি তীরের পেছনের কারণ (“কার FIN, কার ACK বাকি”) বুঝে নিলে পুরো ছবি নিজেই আঁকা যায়।

State machine চালিয়ে দেখা

নিচের সিমুলেটরে দুই প্রান্ত A আর B. প্রতিটি “app” ঘটনা প্রোগ্রামের ডাক (listen, connect, close, abort), আর “gets” মানে পথে থাকা segment পৌঁছাল। একটি ছোট table-এ “বর্তমান অবস্থা + ঘটনা → নতুন অবস্থা + পাঠানো segment” লেখা; চারটি গল্প চালানো হয়েছে: স্বাভাবিক, একসাথে বন্ধ, বন্ধ port-এ SYN, আর abort.

// TCP state machine (RFC 9293 simplified): app calls + arriving segments -> new state + reply.
const T = {
  'CLOSED listen': ['LISTEN'],               'CLOSED connect': ['SYN-SENT', 'SYN'],
  'CLOSED SYN': ['CLOSED', 'RST'],           'LISTEN SYN': ['SYN-RECEIVED', 'SYN+ACK'],
  'SYN-SENT SYN+ACK': ['ESTABLISHED', 'ACK'], 'SYN-SENT RST': ['CLOSED'],
  'SYN-RECEIVED ACK': ['ESTABLISHED'],       'ESTABLISHED close': ['FIN-WAIT-1', 'FIN'],
  'ESTABLISHED FIN': ['CLOSE-WAIT', 'ACK'],  'ESTABLISHED abort': ['CLOSED', 'RST'],
  'ESTABLISHED RST': ['CLOSED'],             'FIN-WAIT-1 ACK': ['FIN-WAIT-2'],
  'FIN-WAIT-1 FIN': ['CLOSING', 'ACK'],      'FIN-WAIT-2 FIN': ['TIME-WAIT', 'ACK'],
  'CLOSING ACK': ['TIME-WAIT'],              'CLOSE-WAIT close': ['LAST-ACK', 'FIN'],
  'LAST-ACK ACK': ['CLOSED'],                'TIME-WAIT 2MSL': ['CLOSED'],
};
function run(title, steps, start = 'CLOSED') {
  console.log('== ' + title);
  const st = { A: start, B: start }, net = [];   // net = segments in flight (FIFO)
  const fire = (side, ev, how) => {
    const r = T[st[side] + ' ' + ev];
    if (!r) { console.log(`  ${side} ${st[side]} ignores ${ev}`); return; }
    const line = `  ${side} ${st[side].padEnd(12)} --${how} ${ev}`.padEnd(38) + `-> ${r[0].padEnd(12)}`;
    console.log((line + (r[1] ? ` sends ${r[1]}` : '')).trimEnd());
    st[side] = r[0];
    if (r[1]) net.push([side === 'A' ? 'B' : 'A', r[1]]);
  };
  for (const s of steps) {
    if (s === '*') while (net.length) { const [to, seg] = net.shift(); fire(to, seg, 'gets'); }
    else fire(s[0], s.slice(2), s.endsWith('2MSL') ? 'timer' : 'app');
  }
}
run('normal: A = client, B = server', ['B listen', 'A connect', '*', 'A close', '*', 'B close', '*', 'A 2MSL']);
run('simultaneous close', ['A close', 'B close', '*', 'A 2MSL', 'B 2MSL'], 'ESTABLISHED');
run('RST 1: SYN to a closed port', ['A connect', '*']);
run('RST 2: abort instead of FIN', ['A abort', '*'], 'ESTABLISHED');

আউটপুট:

== normal: A = client, B = server
  B CLOSED       --app listen         -> LISTEN
  A CLOSED       --app connect        -> SYN-SENT     sends SYN
  B LISTEN       --gets SYN           -> SYN-RECEIVED sends SYN+ACK
  A SYN-SENT     --gets SYN+ACK       -> ESTABLISHED  sends ACK
  B SYN-RECEIVED --gets ACK           -> ESTABLISHED
  A ESTABLISHED  --app close          -> FIN-WAIT-1   sends FIN
  B ESTABLISHED  --gets FIN           -> CLOSE-WAIT   sends ACK
  A FIN-WAIT-1   --gets ACK           -> FIN-WAIT-2
  B CLOSE-WAIT   --app close          -> LAST-ACK     sends FIN
  A FIN-WAIT-2   --gets FIN           -> TIME-WAIT    sends ACK
  B LAST-ACK     --gets ACK           -> CLOSED
  A TIME-WAIT    --timer 2MSL         -> CLOSED
== simultaneous close
  A ESTABLISHED  --app close          -> FIN-WAIT-1   sends FIN
  B ESTABLISHED  --app close          -> FIN-WAIT-1   sends FIN
  B FIN-WAIT-1   --gets FIN           -> CLOSING      sends ACK
  A FIN-WAIT-1   --gets FIN           -> CLOSING      sends ACK
  A CLOSING      --gets ACK           -> TIME-WAIT
  B CLOSING      --gets ACK           -> TIME-WAIT
  A TIME-WAIT    --timer 2MSL         -> CLOSED
  B TIME-WAIT    --timer 2MSL         -> CLOSED
== RST 1: SYN to a closed port
  A CLOSED       --app connect        -> SYN-SENT     sends SYN
  B CLOSED       --gets SYN           -> CLOSED       sends RST
  A SYN-SENT     --gets RST           -> CLOSED
== RST 2: abort instead of FIN
  A ESTABLISHED  --app abort          -> CLOSED       sends RST
  B ESTABLISHED  --gets RST           -> CLOSED

স্বাভাবিক গল্পে ১২টি পরিবর্তন: handshake-এ ৫টি, বন্ধে ৬টি, আর timer-এ ১টি। খেয়াল করুন, TIME-WAIT-এ যায় শুধু A — যে আগে close() ডেকেছে। একসাথে বন্ধ করলে দুই FIN পথে একে অপরকে অতিক্রম করে; দুজনেই FIN-WAIT-1-এ থেকে অন্যের FIN পায়, তাই দুজনেই CLOSING হয়ে শেষে দুজনেই TIME-WAIT-এ যায়। RST-এর দুই গল্পে কোনো ধাপ নেই: বন্ধ port-এ SYN পেয়ে B (কোনো socket ছাড়াই, kernel নিজে) RST পাঠায়, A সরাসরি CLOSED — প্রোগ্রাম পায় ECONNREFUSED. আর abort-এ কোনো FIN-ACK চক্র নেই, দুজনেই এক ধাপে CLOSED, কোনো TIME-WAIT নেই।

৫ · সংযোগ বন্ধ করা — FIN, half-close আর TIME-WAIT

TCP full-duplex, তাই দুই দিকের প্রবাহ আলাদা করে বন্ধ করতে হয়। যে দিকে আর ডেটা যাবে না, সেই দিকের প্রেরক FIN পাঠায়, অন্যজন ACK দেয়। দুই দিক × (FIN + ACK) = চারটি segment — একে প্রায়ই “four-way” বন্ধ বলা হয়। মাঝের ACK আর FIN কখনো কখনো একসাথে যায়, তখন তিনটি segment-এই কাজ শেষ। আগের উদাহরণের সংযোগটিই চলুক: client ৩০০ বাইট পাঠিয়েছে (পরের বাইট ১৩০১), server ৩০০ বাইট (পরের বাইট ৫৩০১)। এবার client বন্ধ করছে:

Client আগে বন্ধ করছে (active close)
তারে যা গেল
Client-এর অবস্থা
Server-এর অবস্থা
সময়
  1. শুরু। দুই দিকেই ডেটা পৌঁছে গেছে আর স্বীকৃত। Client-এর পরের নম্বর ১৩০১, server-এর ৫৩০১।
  2. Client-এর FIN. Client-এর প্রোগ্রাম close() (বা shutdown(SHUT_WR)) ডাকল। Kernel FIN পাঠাল seq ১৩০১ দিয়ে — ডেটা নেই, তবু FIN একটি নম্বর খরচ করে, যেমন SYN করে। Client এখন FIN-WAIT-1.
  3. Server-এর ACK. Server-এর kernel বলল “১৩০২ চাই” — FIN পেয়েছি। Server-এর প্রোগ্রাম এখন read() করলে পাবে ০, অর্থাৎ end of file. Server CLOSE-WAIT-এ, client FIN-WAIT-2-এ। এই মুহূর্তে সংযোগ অর্ধেক বন্ধ: server চাইলে আরও ডেটা পাঠাতে পারে, client তা পড়তে পারে।
  4. Server-এর FIN. Server-এর প্রোগ্রাম কাজ শেষ করে close() ডাকল। FIN গেল seq ৫৩০১ দিয়ে (server এর মধ্যে আর ডেটা পাঠায়নি)। Server LAST-ACK-এ — শুধু শেষ ACK-এর অপেক্ষা।
  5. শেষ ACK আর TIME-WAIT. Client বলল “৫৩০২ চাই”। Server এটা পেয়ে সঙ্গে সঙ্গে CLOSED — তার খাতা বন্ধ। কিন্তু client সরাসরি CLOSED হয় না, TIME-WAIT-এ যায়। কারণ: এই শেষ ACK হারালে server আবার FIN পাঠাবে, আর তখন client-কে সেটা চিনে আবার ACK দিতে হবে।
  6. 2·MSL পরে CLOSED. নিয়ম অনুযায়ী client ২ × MSL অপেক্ষা করে, যাতে এই সংযোগের পথে থাকা সব পুরোনো segment মরে যায়। RFC 793-এ MSL-এর উদাহরণ ২ মিনিট (তাহলে ৪ মিনিট), কিন্তু Linux kernel-এ TIME-WAIT একটি নির্দিষ্ট ৬০ সেকেন্ড। তারপর client-এর চার-মানের জোড়াটি আবার ব্যবহারযোগ্য।

সংখ্যা মিলিয়ে দেখা — পরীক্ষার মতো seq/ack হিসাব

পরীক্ষায় প্রায়ই দেওয়া থাকে দুই ISN আর কে কত বাইট পাঠাল; লিখতে হয় প্রতিটি segment-এর seq আর ack. নিয়ম মাত্র তিনটি: (১) segment-এর seq = প্রেরকের পরের বাইটের নম্বর; (২) ack = প্রাপক পরের যে বাইট চায়; (৩) ডেটা len বাইট খরচ করে, SYN আর FIN একটি করে, খালি ACK কিছুই না। নিচের হিসাবযন্ত্র ঠিক এটুকুই করে, আর নম্বর ২৩২-এ ঘুরে আসে (modulo):

// Exam-style TCP seq/ack calculator: handshake, data, four-way close (no loss, in order).
const M = 2 ** 32;                                   // sequence numbers wrap at 2^32
function trace(title, isnC, isnS, events) {
  console.log(`== ${title}  (ISN client=${isnC}, ISN server=${isnS})`);
  const s = { C: { nxt: isnC, rcv: null }, S: { nxt: isnS, rcv: null } };
  let n = 0;
  const send = (who, flags, len) => {
    const me = s[who], peer = s[who === 'C' ? 'S' : 'C'];
    const seq = me.nxt, ack = flags.includes('ACK') ? String(me.rcv) : '-';
    const used = len + (/SYN|FIN/.test(flags) ? 1 : 0);   // SYN and FIN each use 1 number
    me.nxt = (me.nxt + used) % M;
    peer.rcv = me.nxt;                               // receiver now expects this byte next
    console.log(`${String(++n).padStart(2)}. ${who === 'C' ? 'C->S' : 'S->C'} ${flags.padEnd(8)}` +
      ` seq=${String(seq).padEnd(10)} ack=${ack.padEnd(10)} len=${String(len).padEnd(4)} uses ${used}`);
  };
  send('C', 'SYN', 0); send('S', 'SYN+ACK', 0); send('C', 'ACK', 0);
  for (const e of events) {
    const [who, what] = e.split(' ');
    if (what === 'FIN') send(who, 'FIN+ACK', 0);
    else if (what === 'ACK') send(who, 'ACK', 0);
    else send(who, 'PSH+ACK', Number(what));
  }
  console.log(`   next byte: client ${s.C.nxt}, server ${s.S.nxt}`);
}
trace('exam question', 1000, 5000,
  ['C 100', 'C 200', 'S ACK', 'S 300', 'C ACK', 'C FIN', 'S ACK', 'S FIN', 'C ACK']);
trace('wrap-around', M - 100, 7000, ['C 150', 'S ACK']);

আউটপুট:

== exam question  (ISN client=1000, ISN server=5000)
 1. C->S SYN      seq=1000       ack=-          len=0    uses 1
 2. S->C SYN+ACK  seq=5000       ack=1001       len=0    uses 1
 3. C->S ACK      seq=1001       ack=5001       len=0    uses 0
 4. C->S PSH+ACK  seq=1001       ack=5001       len=100  uses 100
 5. C->S PSH+ACK  seq=1101       ack=5001       len=200  uses 200
 6. S->C ACK      seq=5001       ack=1301       len=0    uses 0
 7. S->C PSH+ACK  seq=5001       ack=1301       len=300  uses 300
 8. C->S ACK      seq=1301       ack=5301       len=0    uses 0
 9. C->S FIN+ACK  seq=1301       ack=5301       len=0    uses 1
10. S->C ACK      seq=5301       ack=1302       len=0    uses 0
11. S->C FIN+ACK  seq=5301       ack=1302       len=0    uses 1
12. C->S ACK      seq=1302       ack=5302       len=0    uses 0
   next byte: client 1302, server 5302
== wrap-around  (ISN client=4294967196, ISN server=7000)
 1. C->S SYN      seq=4294967196 ack=-          len=0    uses 1
 2. S->C SYN+ACK  seq=7000       ack=4294967197 len=0    uses 1
 3. C->S ACK      seq=4294967197 ack=7001       len=0    uses 0
 4. C->S PSH+ACK  seq=4294967197 ack=7001       len=150  uses 150
 5. S->C ACK      seq=7001       ack=51         len=0    uses 0
   next byte: client 51, server 7001

প্রথম হিসাবের সারি ৩ আর ৪ খেয়াল করুন: খালি ACK আর প্রথম ডেটার seq একই 1001. সারি ৬-এ server একটিই ACK দিল 1301 — দুটি segment (100 + 200) একসাথে স্বীকার, এটাই cumulative ACK. সারি ৯–১২ হুবহু ওপরের বন্ধ করার ডায়াগ্রাম: FIN 1301 নিল, তাই ack 1302; server-এর FIN 5301 নিল, তাই শেষ ack 5302. দ্বিতীয় হিসাবে client-এর ISN ২৩২ − ১০০ = 4294967196. SYN-এর পর ডেটা শুরু 4294967197-এ, ১৫০ বাইট পরে নম্বর ২৩২ পেরিয়ে শূন্যে ফিরে 51-এ দাঁড়ায় — তাই server-এর ack 51. Sequence number একটি ঘড়ির কাঁটার মতো ঘোরে; TCP তুলনা করে modulo ২৩২ হিসাবে।

Half-close — এক দিক বন্ধ, অন্য দিক খোলা

close() দুই দিকই ছেড়ে দেয়: প্রোগ্রাম আর পড়তেও পারে না, লিখতেও না। কিন্তু অনেক সময় দরকার শুধু “আমার পাঠানো শেষ” জানানো, উত্তর তখনো আসবে। যেমন client একটি ফাইল পাঠিয়ে বলবে “শেষ”, আর server পুরো ফাইল পেয়ে তবে হিসাব করে উত্তর দেবে। এর জন্য shutdown(fd, SHUT_WR) — এটি FIN পাঠায় কিন্তু পড়ার দিক খোলা রাখে। একে বলে half-close. নিচের C প্রোগ্রামে fork() দিয়ে একই প্রোগ্রামে server আর client চলে, loopback-এ (127.0.0.1). Server parent, client child; সব লাইন ঠিক ক্রমে আসে, কারণ client কিছু ছাপে শুধু server-এর উত্তর পাওয়ার পরে।

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);              /* no buffering: lines appear in real order */
    struct sockaddr_in a = {0};
    a.sin_family = AF_INET; a.sin_port = htons(47290);
    a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);    /* 127.0.0.1 */
    int ls = socket(AF_INET, SOCK_STREAM, 0);      /* SOCK_STREAM = TCP */
    if (bind(ls, (struct sockaddr *)&a, sizeof a) < 0 || listen(ls, 8) < 0) { perror("listen"); return 1; }
    char buf[64]; ssize_t n;  printf("server: listen() on 127.0.0.1:47290      -> LISTEN\n");
    if (fork() == 0) {                             /* child process = client */
        int c = socket(AF_INET, SOCK_STREAM, 0);
        if (connect(c, (struct sockaddr *)&a, sizeof a) < 0) { perror("connect"); _exit(1); }
        if (write(c, "hello", 5) != 5) _exit(1);
        shutdown(c, SHUT_WR);                      /* half-close: send FIN, keep reading */
        n = read(c, buf, sizeof buf - 1); buf[n > 0 ? n : 0] = '\0';
        printf("client: connect, write 5, shutdown(SHUT_WR) -> FIN-WAIT-2\n");
        printf("client: still reading after half-close: \"%s\"\n", buf);
        printf("client: read() = %zd, server FIN arrived   -> TIME-WAIT\n", read(c, buf, sizeof buf));
        close(c); _exit(0);
    }
    int s = accept(ls, NULL, NULL);                /* handshake was already done by the kernel */
    printf("server: accept() gave a new socket       -> ESTABLISHED\n");
    n = read(s, buf, sizeof buf - 1); buf[n > 0 ? n : 0] = '\0';
    printf("server: read() = %zd bytes \"%s\"\n", n, buf);
    printf("server: read() = %zd, EOF (client FIN)    -> CLOSE-WAIT\n", read(s, buf, sizeof buf));
    if (write(s, "bye from server", 15) != 15) return 1;   /* half-closed peer can still receive */
    close(s); wait(NULL);                          /* our FIN -> LAST-ACK; client -> TIME-WAIT */
    printf("server: wrote 15 bytes, close()          -> LAST-ACK, CLOSED\n");
    close(ls); return 0;
}

আউটপুট:

server: listen() on 127.0.0.1:47290      -> LISTEN
server: accept() gave a new socket       -> ESTABLISHED
server: read() = 5 bytes "hello"
server: read() = 0, EOF (client FIN)    -> CLOSE-WAIT
client: connect, write 5, shutdown(SHUT_WR) -> FIN-WAIT-2
client: still reading after half-close: "bye from server"
client: read() = 0, server FIN arrived   -> TIME-WAIT
server: wrote 15 bytes, close()          -> LAST-ACK, CLOSED

পুরো ঘটনাটি ধাপে ধাপে মেলান। accept() ফেরার আগেই handshake শেষ — kernel সেটা নিজে করেছে, প্রোগ্রাম শুধু তৈরি সংযোগটি তুলে নিয়েছে। Server প্রথম read()-এ ৫ বাইট পেল, দ্বিতীয়টিতে ০ — client-এর shutdown(SHUT_WR) FIN পাঠিয়েছে, আর TCP-তে FIN-এর প্রোগ্রাম-স্তরের রূপই “EOF”. তবু server-এর লেখা ১৫ বাইট client পড়তে পারল: "bye from server" — half-close-এর আসল প্রমাণ। এরপর client-এর read() = ০, অর্থাৎ server-ও FIN পাঠিয়েছে। যেহেতু client আগে FIN দিয়েছিল, TIME-WAIT-এ যায় client-এর socket; server LAST-ACK থেকে শেষ ACK পেয়ে CLOSED. ডান পাশের state-গুলো প্রোগ্রাম নিজে মাপেনি, ঘটনাক্রম থেকে লেখা — আসল kernel-এর state পরের অংশে ss দিয়ে দেখব।

TIME-WAIT কেন আছে — দুটি কারণ

  1. শেষ ACK হারালে: server LAST-ACK-এ বসে থেকে আবার FIN পাঠাবে। Client যদি ততক্ষণে সব ভুলে যেত, সে এই অচেনা FIN-এর উত্তরে RST পাঠাত, আর server-এর প্রোগ্রাম একটি সফল সংযোগের শেষে অকারণে error দেখত। TIME-WAIT-এ থাকা client FIN চিনে আবার ACK দেয়।
  2. দেরিতে আসা পুরোনো segment: একই চার-মানের জোড়ায় সঙ্গে সঙ্গে নতুন সংযোগ খুললে, পুরোনো সংযোগের কোনো দেরি-হওয়া segment (কোথাও router-এর queue-তে আটকে ছিল) নতুন সংযোগের ডেটা হিসেবে ঢুকে যেতে পারত। ২·MSL অপেক্ষা মানে এক দিকে যাওয়া আর উত্তর ফেরার সর্বোচ্চ আয়ু — এর মধ্যে সব পুরোনো segment মরে যায়।

খেয়াল করুন, TIME-WAIT-এর বোঝা বহন করে active close করা পক্ষ — যে আগে FIN দেয়। RFC-র উদাহরণ MSL ২ মিনিট, ফলে TIME-WAIT ৪ মিনিট হতো; বাস্তব OS-গুলো ছোট মান নেয়। Linux-এ TIME-WAIT kernel-এ বাঁধা ৬০ সেকেন্ড (TCP_TIMEWAIT_LEN), sysctl দিয়ে বদলানো যায় না বলেই জানা যায়; Windows-এ ডিফল্ট সাধারণত ১২০ সেকেন্ড বলে উল্লেখ আছে, registry দিয়ে বদলানো যায়। একটি প্রচলিত ভুল ধারণা: Linux-এর tcp_fin_timeout TIME-WAIT-এর দৈর্ঘ্য নয় — এটি মূলত FIN-WAIT-2-এ অনাথ (orphan) সংযোগ কতক্ষণ থাকবে তা ঠিক করে।

ব্যস্ত client-এ হাজার হাজার TIME-WAIT — ঠিক আর ভুল সমাধান

একটি সার্ভিস প্রতি অনুরোধে ডেটাবেস বা অন্য API-তে নতুন TCP সংযোগ খুলে আবার বন্ধ করলে, প্রতিটি সংযোগ ৬০ সেকেন্ড TIME-WAIT-এ পড়ে থাকে। একই গন্তব্য (IP, port)-এ নতুন সংযোগের জন্য আলাদা উৎস port লাগে, আর Linux-এর ephemeral পরিসর 32768–60999 মানে ২৮,২৩২টি port. তাই একই গন্তব্যে টানা হারে সর্বোচ্চ প্রায় ২৮২৩২ ÷ ৬০ ≈ ৪৭০টি নতুন সংযোগ প্রতি সেকেন্ডে — এর বেশি হলে connect() EADDRNOTAVAIL দিতে শুরু করে। এটাই ephemeral port exhaustion.

উপায়রায়কেন
সংযোগ বারবার ব্যবহার — HTTP keep-alive, connection pool (persistent connection)✅ সেরাHandshake আর TIME-WAIT দুটোই প্রতি অনুরোধে আর লাগে না; latency-ও কমে।
নকশা বদলে client-কে আগে বন্ধ করতে দেওয়া✅ ভালোTIME-WAIT তখন হাজার client-এর মধ্যে ছড়িয়ে যায়, এক server-এ জমে না।
ephemeral পরিসর বাড়ানো, বা গন্তব্য IP/port বেশি রাখা✅ সহায়কচার-মানের জোড়া বেশি হলে একই সময়ে বেশি সংযোগ সম্ভব।
Linux-এ net.ipv4.tcp_tw_reuse (বাইরে যাওয়া সংযোগের জন্য)⚠️ শর্তসাপেক্ষTimestamp দিয়ে পুরোনো segment চিনতে পারলে TIME-WAIT-এর জোড়া নতুন connect()-এ আবার ব্যবহার করে; বুঝে-শুনে চালু করতে হয়।
Server-এ SO_REUSEADDR✅ কিন্তু অন্য সমস্যারRestart-এর পর পুরোনো সংযোগ TIME-WAIT-এ থাকলেও listen port-এ আবার bind() করতে দেয়। TIME-WAIT কমায় না।
পুরোনো tcp_tw_recycle❌ ভুলNAT-এর পেছনের অনেক client-এর timestamp গুলিয়ে সংযোগ ফেলে দিত; Linux ৪.১২ (২০১৭)-এ সরিয়ে ফেলা হয়েছে।
SO_LINGER ০ দিয়ে সব সংযোগ RST-এ বন্ধ❌ ভুলTIME-WAIT এড়ায় ঠিকই, কিন্তু পথে থাকা ডেটা হারাতে পারে আর অন্য পক্ষ error দেখে।

SO_REUSEADDR-এর অর্থ নিয়ে বিভ্রান্তি খুব সাধারণ। Linux-এ এটি দিলে একটি listening socket সেই port-এ bind() করতে পারে, যদিও আগের চালানোর সংযোগগুলো TIME-WAIT-এ পড়ে আছে — না দিলে restart-এর পর এক মিনিট Address already in use. এটি TIME-WAIT মোছে না, পুরোনো segment-এর সুরক্ষাও নষ্ট করে না, কারণ চার-মানের জোড়া নতুন client-দের ক্ষেত্রে আলাদা। BSD/macOS আর Windows-এ এই option-এর সূক্ষ্ম আচরণ আলাদা (Windows-এ এর অর্থ আরও বিপজ্জনকভাবে ঢিলা, তাই সেখানে SO_EXCLUSIVEADDRUSE সুপারিশ করা হয়) — তাই অন্য OS-এ কোড নেওয়ার আগে তার নথি দেখে নিন।

৬ · নিজের চোখে state দেখা, আর RST — হঠাৎ বন্ধ

Linux-এ প্রতিটি TCP socket-এর বর্তমান অবস্থা দেখায় ss -tan (t = TCP, a = সব, n = নাম নয় সংখ্যা)। পুরোনো netstat -tan-ও একই কাজ করত, কিন্তু অনেক নতুন distribution-এ সেটি আর ডিফল্টে থাকে না। নিচের প্রোগ্রাম একটি process-এর ভেতরেই loopback-এ server (port 47291) আর client (port 47292) খোলে, তিনটি মুহূর্তে ss চালায় — ss না থাকলে /proc/net/tcp থেকে কাঁচা সারি দেখায় — আর শেষে এমন একটি port-এ connect() করে যেখানে কেউ শুনছে না।

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

static struct sockaddr_in at(unsigned short port) {
    struct sockaddr_in a = {0};
    a.sin_family = AF_INET; a.sin_port = htons(port);
    a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    return a;
}
static void show(const char *when) {               /* ss if present, else raw /proc/net/tcp */
    printf("--- %s\n", when); fflush(stdout);
    if (system("command -v ss >/dev/null && ss -tan | awk 'NR==1 || /:4729[12]/' "
               "|| grep -i ':B8B[BC] ' /proc/net/tcp") != 0) printf("(no output)\n");
}
int main(void) {
    struct sockaddr_in srv = at(47291), cli = at(47292), off = at(47299);
    int ls = socket(AF_INET, SOCK_STREAM, 0), c = socket(AF_INET, SOCK_STREAM, 0);
    bind(ls, (struct sockaddr *)&srv, sizeof srv); listen(ls, 8);
    bind(c, (struct sockaddr *)&cli, sizeof cli);  /* fixed client port, easy to spot */
    if (connect(c, (struct sockaddr *)&srv, sizeof srv) < 0) { perror("connect"); return 1; }
    int s = accept(ls, NULL, NULL);
    show("after connect + accept");
    close(c);  show("client closed first (active close)");
    close(s);  show("server closed too");
    int z = socket(AF_INET, SOCK_STREAM, 0);
    if (connect(z, (struct sockaddr *)&off, sizeof off) < 0)
        printf("--- connect to 127.0.0.1:47299 (nobody listening): %s (errno %d)\n", strerror(errno), errno);
    return 0;
}

আউটপুট:

--- after connect + accept
State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN 0      8          127.0.0.1:47291      0.0.0.0:*
ESTAB  0      0          127.0.0.1:47291    127.0.0.1:47292
ESTAB  0      0          127.0.0.1:47292    127.0.0.1:47291
--- client closed first (active close)
State      Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN     0      8          127.0.0.1:47291      0.0.0.0:*
CLOSE-WAIT 1      0          127.0.0.1:47291    127.0.0.1:47292
FIN-WAIT-2 0      0          127.0.0.1:47292    127.0.0.1:47291
--- server closed too
State     Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
LISTEN    0      8          127.0.0.1:47291      0.0.0.0:*
TIME-WAIT 0      0          127.0.0.1:47292    127.0.0.1:47291
--- connect to 127.0.0.1:47299 (nobody listening): Connection refused (errno 111)

প্রথম ছবিতে তিনটি সারি: LISTEN socket আর একটি সংযোগের দুই প্রান্ত (loopback-এ দুই প্রান্তই এক যন্ত্রে, তাই দুটি ESTAB). LISTEN সারির Send-Q = 8 — এটি আমাদের listen(ls, 8)-এর backlog; LISTEN socket-এর জন্য ss এই ঘরে backlog আর Recv-Q-তে accept queue-এর বর্তমান দৈর্ঘ্য দেখায়। Client close() করার পর সার্ভারের দিক CLOSE-WAIT, client-এর দিক FIN-WAIT-2 — হুবহু ওপরের stepper-এর ধাপ ৩। CLOSE-WAIT-এর Recv-Q = 1 লক্ষ করার মতো: Linux FIN-কেও প্রাপ্তির হিসাবে একটি নম্বর হিসেবে গোনে, যা প্রোগ্রাম এখনো “পড়েনি”। Server-ও বন্ধ করলে সার্ভারের সারি মিলিয়ে যায় (LAST-ACK থেকে CLOSED), আর থাকে শুধু client-এর TIME-WAIT — ৬০ সেকেন্ড ধরে। শেষ লাইন: 47299-এ কেউ নেই, kernel SYN-এর উত্তরে RST দিয়েছে, তাই connect() পেল ECONNREFUSED (Linux-এ errno 111). চালানোর পরিবেশভেদে কলামের ফাঁকা জায়গা একটু আলাদা হতে পারে।

RST কখন আসে

FIN হলো ভদ্রভাবে বিদায় — “আমার কথা শেষ, আপনার শেষ হলে জানাবেন”, কোনো ডেটা হারায় না। RST হলো ফোন কেটে দেওয়া — সংযোগ এখনই শেষ, পথে বা buffer-এ থাকা ডেটা বাতিল, কোনো TIME-WAIT নেই, আর অন্য পক্ষের প্রোগ্রাম সাধারণত error পায়। RST পাঠানোর প্রধান পরিস্থিতি:

  • বন্ধ port-এ SYN: কেউ শুনছে না — উত্তরে RST, client পায় Connection refused. (Firewall থাকলে প্রায়ই কোনো উত্তরই আসে না; তখন client বারবার SYN পাঠিয়ে শেষে timeout পায় — “refused” আর “timed out”-এর পার্থক্য সমস্যা খুঁজতে খুব কাজের।)
  • Abort: প্রোগ্রাম SO_LINGER শূন্য দিয়ে বন্ধ করলে, অথবা Linux-এ এখনো না-পড়া ডেটা receive buffer-এ রেখে close() করলে kernel FIN-এর বদলে RST পাঠায়। অন্য পক্ষ দেখে Connection reset by peer (ECONNRESET).
  • Half-open সংযোগ ধরা পড়া: এক পক্ষ crash বা reboot করে সব ভুলে গেছে, অন্য পক্ষ জানে না। পুরোনো পক্ষ ডেটা পাঠালে নতুন-জাগা পক্ষ অচেনা segment দেখে RST দেয় — তখন সংযোগটি পরিষ্কার হয়।
  • অচেনা বা অবৈধ segment: যেমন handshake-এ ভুল ack নম্বরের SYN+ACK (পুরোনো নকল SYN-এর গল্প)।

RST যেহেতু এক segment-এই সংযোগ শেষ করে, কেউ RST জাল করতে পারলে অন্যের সংযোগ ভেঙে দিতে পারত। তাই আধুনিক TCP (RFC 5961) RST গ্রহণ করে কেবল তখনই, যখন তার seq ঠিক প্রত্যাশিত নম্বরের সাথে মেলে; window-এর ভেতরে কিন্তু ঠিক না মিললে একটি “challenge ACK” পাঠিয়ে যাচাই করে। ISN এলোমেলো রাখার মতোই, এটিও অনুমান-অযোগ্যতার ওপর দাঁড়ানো প্রতিরক্ষা।

৭ · Listen backlog, SYN flood আর SYN cookie

Server-এর kernel প্রতিটি listening socket-এর জন্য দুটি queue রাখে:

  • SYN queue: SYN এসেছে, SYN+ACK গেছে, শেষ ACK-এর অপেক্ষা — SYN-RECEIVED অবস্থার অর্ধেক-তৈরি সংযোগ। প্রতিটির জন্য একটি ছোট সারি (ঠিকানা, ISN, option) রাখতে হয়। Linux-এ এর আকারের সাথে tcp_max_syn_backlog জড়িত (আমাদের চালানোর পরিবেশে মান 128 দেখা গেছে; যন্ত্রভেদে আলাদা)।
  • Accept queue: Handshake শেষ, ESTABLISHED, কিন্তু প্রোগ্রাম এখনো accept() দিয়ে তোলেনি। আকার listen()-এর backlog, যা net.core.somaxconn দিয়ে সীমিত (নতুন Linux-এ ডিফল্ট 4096, পুরোনোগুলোতে 128).

অফিসের রিসেপশনের কথা ভাবুন। দর্শনার্থী এসে নাম বললেন, রিসেপশনিস্ট খাতায় লিখে বললেন “পরিচয়পত্র দেখান” — এই অর্ধেক-সম্পন্ন অবস্থা SYN queue. পরিচয় মিলল, তিনি ওয়েটিং রুমে বসলেন — accept queue. কর্মকর্তা একে একে ডেকে নেন — accept(). ওয়েটিং রুম ভরে গেলে নতুন যাচাই-হওয়া দর্শনার্থী বসার জায়গা পান না; Linux সে ক্ষেত্রে ডিফল্টে শেষ ACK চুপচাপ ফেলে দেয়, client ধরে নেয় সংযোগ হয়ে গেছে, আর server পরে SYN+ACK আবার পাঠিয়ে সুযোগ দেয়। Accept queue ভরে যাওয়া মানে প্রায় সবসময় প্রোগ্রাম accept() যথেষ্ট দ্রুত ডাকছে না (পর্ব ৪৩–৪৪-এর বিষয়)।

SYN flood — রিসেপশনের খাতা ভরিয়ে দেওয়া

এখন ভাবুন, কেউ হাজার হাজার ভুয়া নামে দর্শনার্থী পাঠাল, যারা নাম লিখিয়েই চলে যায়, কখনো পরিচয়পত্র দেখায় না। খাতা ভরে যাবে, আসল দর্শনার্থী আর নাম লেখাতে পারবেন না। এটাই SYN flood: আক্রমণকারী প্রচুর SYN পাঠায়, প্রায়ই নকল উৎস ঠিকানা দিয়ে, তাই SYN+ACK এমন ঠিকানায় যায় যারা কখনো ACK দেবে না। প্রতিটি অর্ধেক-সংযোগ server-এর SYN queue-তে জায়গা দখল করে কয়েক দফা পুনঃপ্রেরণ পর্যন্ত বসে থাকে। এটি একটি denial-of-service আক্রমণ — server-এর CPU বা bandwidth নয়, ফুরায় তার অর্ধেক-সংযোগের খাতা। এখানে শুধু ধারণা আর প্রতিরক্ষা; বড় আকারের DDoS-এর প্রতিরক্ষা পর্ব ৫০-এ।

প্রতিরক্ষার কয়েকটি স্তর আছে: ISP-র ingress filtering নকল উৎস ঠিকানা কমায়; বড় SYN queue আর কম পুনঃপ্রেরণ দিয়ে প্রতিটি অর্ধেক-সংযোগের আয়ু কমানো যায়; বড় সেবাগুলো scrubbing center বা CDN-এর পেছনে থাকে। কিন্তু সবচেয়ে চমৎকার কৌশলটি server-এর নিজের ভেতরে — SYN cookie, যা ১৯৯৬ সালে Daniel J. Bernstein আর Eric Schenk-এর কাজ থেকে এসেছে।

SYN cookie — অবস্থা না রেখে ISN-এ লুকানো তথ্য

মূল চিন্তা: queue ভরে গেলে server SYN-এর জন্য কোনো সারিই রাখবে না। বরং দরকারি তথ্যগুলো ঢুকিয়ে দেবে নিজের ISN-এর ৩২ bit-এ — যা SYN+ACK-এ client-এর কাছে যায় আর client-কে ACK-এ ack = ISN + 1 হিসেবে ফেরত দিতেই হয়। Bernstein-এর মূল নকশা (ধারণাগতভাবে):

ISN(server) = [ t : 5 bit ][ m : 3 bit ][ s : 24 bit ]
 t = ধীরে বাড়া একটি সময়-গণক (মোটামুটি প্রতি ৬৪ সেকেন্ডে ১), mod 32
 m = client-এর MSS-এর কাছাকাছি একটি মানক মানের সূচক (৮টির মধ্যে একটি)
 s = গোপন চাবি + client IP, client port, server IP, server port + t -এর hash (২৪ bit)

নিচের উদাহরণে client 198.51.100.7:52000 থেকে server 203.0.113.10:443-এ আসছে, client-এর ISN ৭০০০, আর hash-এর মানটি বোঝানোর জন্য বানানো একটি সংখ্যা।

Server-এর SYN queue ভর্তি — cookie মোড চালু
তারে যা গেল
Server-এর হিসাব
যাচাই
ফল
  1. SYN এলো, queue ভর্তি। Server বর্তমান সময়-গণক t = ১৭ নিল, client-এর MSS ১৪৬০-কে তালিকার সূচক m = ৩ দিয়ে প্রকাশ করল, আর গোপন চাবি, চার-মানের জোড়া ও t মিলিয়ে একটি ২৪-bit hash s বানাল।
  2. Cookie-ই ISN. তিন টুকরো জুড়ে ৩২-bit সংখ্যা: ১৭ × ২২৭ + ৩ × ২২৪ + 0x5A1C3E = 0x8B5A1C3E (দশমিকে 2337938494). এটাই server-এর ISN হিসেবে SYN+ACK-এ গেল। Client-এর চোখে এটি একটি সাধারণ এলোমেলো ISN-এর মতোই।
  3. মনে রাখার কিছু নেই। Server কোনো সারি রাখল না। হাজারটি ভুয়া SYN এলেও মেমরি খরচ শূন্য; শুধু প্রতিটির জন্য একটি hash হিসাব আর একটি SYN+ACK পাঠানোর খরচ। নকল ঠিকানার কেউ কখনো ACK দেবে না — কিছুই হারাল না।
  4. আসল client-এর ACK. ack = 0x8B5A1C3F এলো। Server এক বিয়োগ করে cookie পেল, ওপরের ৫ bit থেকে t = ১৭, পরের ৩ bit থেকে m = ৩। এখন t = ১৮, অর্থাৎ cookie মাত্র এক গণক পুরোনো — গ্রহণযোগ্য। একই চাবি আর একই চার-মানের জোড়া দিয়ে hash আবার হিসাব করে 0x5A1C3E পাওয়া গেল — মিলেছে। এখনই সংযোগের সারি তৈরি হলো, MSS ১৪৬০ ধরে।
  5. জাল ACK. কেউ যদি SYN না পাঠিয়ে বা অন্য port থেকে ACK পাঠায়, চার-মানের জোড়া আলাদা হওয়ায় hash মিলবে না, আর গোপন চাবি না জানলে মেলানো সম্ভবও নয়। Segment ফেলে দেওয়া হয়। তাই cookie একসাথে “মনে রাখা” আর “যাচাই” দুটোর কাজ করে।

দাম কী? মাত্র ৩২ bit-এ সব তথ্য আঁটে না। মূল নকশায় MSS শুধু ৮টি মানের একটিতে নামিয়ে আনা যায়, আর window scale, SACK-এর মতো option হারিয়ে যায় — ফলে cookie দিয়ে তৈরি সংযোগ কিছুটা দুর্বল চলে। Linux-এর আসল সূত্র এই মূল নকশা থেকে কিছুটা আলাদা, আর timestamp চালু থাকলে কিছু option-এর তথ্য timestamp-এর কয়েকটি bit-এ লুকিয়ে বাঁচানো হয়। এজন্য Linux ডিফল্টে (tcp_syncookies = 1) cookie ব্যবহার করে কেবল SYN queue ভরে গেলে, সবসময় নয়।

৮ · Keepalive, TCP Fast Open আর handshake-এর খরচ

TCP keepalive — নীরব সংযোগ বেঁচে আছে তো?

TCP নিজে থেকে নীরব সংযোগে কিছু পাঠায় না। দুই পক্ষ এক ঘণ্টা চুপ থাকলে সংযোগ এক ঘণ্টা ESTABLISHED-ই থাকে — এমনকি অন্য পক্ষ বিদ্যুৎ চলে গিয়ে বন্ধ হয়ে গেলেও। TCP keepalive এর সমাধান: socket-এ SO_KEEPALIVE চালু থাকলে, নির্দিষ্ট সময় নীরবতার পর kernel একটি ছোট probe (ডেটাহীন ACK) পাঠায়; উত্তর না এলে কয়েকবার চেষ্টা করে সংযোগ বাতিল ঘোষণা করে। Linux-এর ডিফল্ট মান অনেক বড়: ৭২০০ সেকেন্ড (২ ঘণ্টা) নীরবতা, তারপর ৭৫ সেকেন্ড পরপর ৯টি probe. তাই বাস্তবে প্রোগ্রাম প্রতি socket-এ ছোট মান বসায় (TCP_KEEPIDLE ইত্যাদি), অথবা application নিজের “ping” বার্তা পাঠায় (যেমন HTTP/2 বা WebSocket-এর ping). আরেকটি বাস্তব কারণ: পথের NAT বা firewall নীরব সংযোগের conntrack সারি কিছুক্ষণ পরে মুছে দেয় (পর্ব ১৯) — তাই অনেক মোবাইল অ্যাপ কয়েক মিনিট পরপর ছোট keepalive পাঠায়।

TCP Fast Open — SYN-এর সাথেই ডেটা

Handshake-এর এক RTT অপেক্ষা ছোট অনুরোধের জন্য বড় খরচ। TCP Fast Open (RFC 7413, ২০১৪, experimental) প্রথম সংযোগে server থেকে একটি cookie নিয়ে রাখে; পরের বার client SYN-এর সাথেই ডেটা আর cookie পাঠায়, আর server cookie যাচাই করে handshake শেষ হওয়ার আগেই ডেটা প্রোগ্রামের হাতে দেয়। এতে পুনঃসংযোগে এক RTT বাঁচে। কিন্তু দুটি সমস্যা: SYN-এর ডেটা পথে নকল হয়ে দুবার পৌঁছাতে পারে, তাই শুধু বারবার চালালেও ক্ষতি নেই এমন (idempotent) অনুরোধে নিরাপদ; আর পথের অনেক middlebox অপরিচিত option বা ডেটাওয়ালা SYN ফেলে দেয়। Linux-এ client দিকে এটি চালু করা যায় (tcp_fastopen), কিন্তু browser আর বড় নেটওয়ার্কে এর ব্যবহার শেষ পর্যন্ত সীমিতই থেকেছে বলে জানা যায়। একই লক্ষ্য QUIC-এর 0-RTT অনেক বেশি সফলভাবে পূরণ করছে (পর্ব ৩৩)।

মোট খরচের হিসাব

পরিস্থিতিঅনুরোধ পাঠানোর আগে RTTRTT = ১৫০ ms হলে
শুধু TCP (যেমন সাধারণ HTTP)১১৫০ ms
TCP + TLS 1.3২৩০০ ms
TCP + TLS 1.2 (পুরোনো)৩৪৫০ ms
QUIC (নতুন সংযোগ)১১৫০ ms
QUIC 0-RTT বা TFO (আগে দেখা server)০০ ms (ঝুঁকির শর্ত মেনে)

এই সারণির পরে উত্তরের জন্য আরও একটি RTT লাগে, সবার ক্ষেত্রেই। সংখ্যাগুলো দেখায় কেন persistent connection, CDN আর QUIC এত গুরুত্বপূর্ণ: প্রতিটি RTT বাঁচানো ব্যবহারকারীর চোখে পেজ দ্রুত খোলা।

আরও গভীরে: Linux-এ TIME-WAIT সংযোগ পূর্ণ socket হিসেবে থাকে না, রাখা হয় একটি ছোট inet_timewait_sock কাঠামোয় — তাই লাখখানেক TIME-WAIT মেমরির দিক থেকে খুব ভারী নয়, সমস্যা মূলত port-এর। Listen socket-এর SYN queue আধুনিক kernel-এ আলাদা queue নয়, বরং মূল connection hash table-এ রাখা “request sock”; আর SO_REUSEPORT দিয়ে কয়েকটি process একই port-এ আলাদা listen socket রেখে নতুন সংযোগ ভাগ করে নিতে পারে। RFC 9293 (২০২২) এখন TCP-র মূল মানদণ্ড; RFC 6528 ISN, RFC 5961 জাল RST/SYN প্রতিরোধ, RFC 7323 window scale ও timestamp, আর RFC 4987 SYN flood-এর প্রতিরক্ষাগুলোর বর্ণনা দেয়।

পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:

  • Handshake তিন ধাপের কেন, দুই নয়? — দুই দিকের ISN-ই পাঠাতে ও স্বীকার করাতে হয়, আর পুরোনো নকল SYN থেকে ভুয়া সংযোগ ঠেকাতে হয়।
  • Client ISN x, কোনো ডেটা ছাড়া SYN+ACK-এর ack কত? — x + 1, কারণ SYN একটি sequence number খরচ করে।
  • TIME-WAIT কে বহন করে আর কতক্ষণ? — যে আগে FIN দেয় (active close), 2·MSL; Linux-এ নির্দিষ্ট ৬০ সেকেন্ড।
  • অনেক CLOSE-WAIT দেখলে কী বুঝবেন? — অন্য পক্ষ বন্ধ করেছে, কিন্তু নিজের প্রোগ্রাম close() ডাকছে না — সাধারণত প্রোগ্রামের বাগ (socket leak).
  • SYN cookie কী বাঁচায়? — SYN queue-এর মেমরি; অবস্থা server-এর ISN-এ এনকোড হয় আর ফেরত ACK-এর ack − 1 থেকে hash মিলিয়ে যাচাই হয়।

৯ · AI-এর যুগে TCP সংযোগ নিয়ে সমস্যা খোঁজা

“আমার সার্ভারে হাজার হাজার TIME-WAIT, কী করব?” — AI-কে জিজ্ঞেস করলে কয়েক সেকেন্ডে একগাদা sysctl পাবেন। কিন্তু AI আপনার যন্ত্রের kernel version, কে আগে বন্ধ করে, বা সমস্যাটা আসলেই TIME-WAIT কি না — কিছুই দেখতে পায় না। চেনা ভুলগুলো:

AI-এর পরামর্শসমস্যাযাচাইয়ের উপায়
tcp_tw_recycle = 1 চালু করুনএই option ২০১৭ সালেই Linux থেকে সরানো; পুরোনো kernel-এ NAT-এর পেছনের client-দের সংযোগ ভাঙত।sysctl -a | grep tw_ দিয়ে আসলে কী আছে দেখুন; kernel-এর নথি পড়ুন।
tcp_fin_timeout কমালে TIME-WAIT কমবেLinux-এ TIME-WAIT ৬০ সেকেন্ডে বাঁধা; এই মান FIN-WAIT-2-এর জন্য।পরিবর্তনের আগে-পরে ss -tan state time-wait | wc -l মাপুন।
“SO_REUSEADDR দিলে TIME-WAIT চলে যাবে”এটি শুধু bind() করতে দেয়; TIME-WAIT থাকে।man 7 socket পড়ুন, ছোট পরীক্ষা চালিয়ে দেখুন।
পুরোনো কমান্ড netstat, ifconfigঅনেক নতুন distribution-এ ডিফল্টে নেই, আউটপুটও আলাদা।ss আর ip ব্যবহার করুন (পর্ব ০৬)।
Firewall-এ “সব port খুলে দিন” বা SYN cookie বন্ধ করুনআক্রমণের দরজা খোলা আর প্রতিরক্ষা সরিয়ে ফেলা।শুধু দরকারি port খুলুন; tcp_syncookies ডিফল্ট রাখুন।
বানানো RFC নম্বর বা seq/ack-এর ভুল হিসাবAI প্রায়ই SYN/FIN-এর ১ নম্বর খরচ ভুলে যায়।এই পর্বের হিসাবযন্ত্রে মিলিয়ে নিন; RFC Editor-এর সাইটে নম্বর খুঁজুন।

AI-কে প্রশ্ন করার সময় আসল তথ্য দিন, আর অনুমান না করে মাপতে বলুন:

আমার Linux সার্ভারে (kernel 6.x, Ubuntu) একটি API সার্ভিস প্রতি অনুরোধে ডেটাবেসে নতুন TCP সংযোগ খোলে। ss -tan state time-wait | wc -l দেখাচ্ছে প্রায় ২৮০০০, আর মাঝে মাঝে connect() EADDRNOTAVAIL দিচ্ছে। ১. আগে বলো কোন পক্ষ active close করছে তা কীভাবে নিশ্চিত হব, আর কোন ss কমান্ডে কী দেখব। ২. মূল সমাধান (connection pool / keep-alive) ধাপে ধাপে বোঝাও, sysctl বদলের আগে। ৩. কোনো sysctl বললে নাম, kernel-এ এখনো আছে কি না, আর ঝুঁকি স্পষ্ট লিখবে। tcp_tw_recycle বলবে না। ৪. প্রতিটি দাবির জন্য man page বা RFC নম্বর দাও, যা আমি নিজে মিলিয়ে নিতে পারব।

AI-এর TCP পরামর্শ প্রয়োগের আগে — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কমান্ড-জানা মানুষ থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১দুই পক্ষ কি কখনো শতভাগ নিশ্চিত হতে পারে যে সংযোগ তৈরি হয়েছে? তিন ধাপে থামা হলো কেন, চার বা পাঁচ নয়?

শতভাগ নিশ্চয়তা অসম্ভব — এটি কম্পিউটার বিজ্ঞানের একটি বিখ্যাত ফল, যাকে বলা হয় “দুই সেনাপতির সমস্যা” (Two Generals’ Problem). দুই পাহাড়ে দুই সেনাপতি, মাঝের উপত্যকা দিয়ে দূত পাঠাতে হয়, দূত ধরা পড়তে পারে। প্রথমজন বার্তা পাঠালেন “ভোরে আক্রমণ”; দ্বিতীয়জন উত্তর দিলেন “রাজি”। কিন্তু দ্বিতীয়জন জানেন না তাঁর উত্তর পৌঁছেছে কি না, তাই নিশ্চিত হতে আরেকটি স্বীকৃতি চাই; তারপর সেটার স্বীকৃতি… যত বার্তাই পাঠানো হোক, শেষ বার্তাটি যে পৌঁছাল তা প্রেরক কখনো জানেন না। হারাতে পারে এমন পথে চূড়ান্ত ঐকমত্য গাণিতিকভাবে অসম্ভব।

তাহলে TCP কী করে? সে অসম্ভবকে চায় না, বরং চায় যথেষ্ট নিশ্চয়তা আর ভুল হলে সুস্থ হয়ে ওঠার উপায়। তিন ধাপে দুই দিকের ISN পাঠানো ও স্বীকার — দুটোই সম্পন্ন হয়। তৃতীয় ACK হারালে কী হয়? Client ভাবে সংযোগ হয়ে গেছে আর ডেটা পাঠায়; সেই ডেটা-segment-এর ভেতরেই ACK থাকে, তাই server-ও ESTABLISHED হয়ে যায়। Client কিছু না পাঠালে server SYN+ACK আবার পাঠায়। অর্থাৎ শেষ বার্তা হারানোর ক্ষতি পরের স্বাভাবিক ট্রাফিকই পুষিয়ে দেয়। চতুর্থ বা পঞ্চম বার্তা যোগ করলে প্রতিটি সংযোগে আরও RTT লাগত, অথচ নিশ্চয়তা মৌলিকভাবে বাড়ত না।

১৯৭৫ সালে Ray Tomlinson-এর প্রস্তাবিত three-way handshake ঠিক এই ভারসাম্যই ধরে — সর্বনিম্ন বার্তায় পুরোনো নকল SYN থেকে সুরক্ষা আর দুই দিকের নম্বর-চুক্তি। সংযোগ বন্ধের বেলাতেও একই দর্শন: শেষ ACK হারাতে পারে, তাই TIME-WAIT অপেক্ষা করে আবার ACK দেওয়ার জন্য — চিরকাল নয়, ২·MSL পর্যন্ত। আধুনিক distributed system-এও একই নীতি — Paxos বা Raft-এর মতো ঐকমত্য protocol timeout আর পুনঃচেষ্টা দিয়ে “বাস্তবে যথেষ্ট” নিশ্চয়তা দেয়।

মূল উপলব্ধি: নেটওয়ার্ক protocol-এর নকশা মানে অসম্ভব নিশ্চয়তা নয়, বরং কম খরচে যথেষ্ট নিশ্চয়তা আর ব্যর্থতা থেকে নিজে নিজে সেরে ওঠার পথ। তিনটি বার্তা সেই হিসাবের সর্বনিম্ন বিন্দু।

প্র ০২ISN অনুমানযোগ্য হলে ঠিক কী বিপদ? ইতিহাসের কোন ঘটনাগুলো আমাদের এই শিক্ষা দিয়েছে?

মূল RFC 793-এর ISN নিয়মের লক্ষ্য ছিল নির্ভরযোগ্যতা, নিরাপত্তা নয়। পুরোনো বহু Unix বাস্তবায়ন ISN বাড়াত সহজ, অনুমানযোগ্য হারে — যেমন প্রতি সেকেন্ডে একটি নির্দিষ্ট মান আর প্রতি সংযোগে আরেকটি নির্দিষ্ট মান।

১৯৮৫ সালে Robert T. Morris একটি প্রতিবেদনে দেখান, এমন অনুমানযোগ্য ISN থাকলে একজন বাইরের ব্যক্তি অন্য কোনো বিশ্বস্ত যন্ত্রের ঠিকানা নকল করে TCP সংযোগ জাল করতে পারেন। কৌশলটা ধারণাগতভাবে এই: নিজে একটি স্বাভাবিক সংযোগ করে সার্ভারের বর্তমান ISN দেখে নেওয়া, তারপর নকল ঠিকানায় SYN পাঠিয়ে পরের ISN অনুমান করে তৃতীয় ACK বসানো — SYN+ACK কখনো চোখে না দেখেই। সেই যুগে অনেক সার্ভার শুধু উৎস IP দেখে বিশ্বাস করত (যেমন rsh-এর মতো পুরোনো সেবা), তাই একটি জাল সংযোগই যথেষ্ট ছিল। ১৯৮৯ সালে Steve Bellovin-ও TCP/IP-এর এই ধরনের দুর্বলতা নিয়ে লেখেন। আর ১৯৯৪ সালের ডিসেম্বরে Kevin Mitnick-এর সাথে যুক্ত বহুল-আলোচিত একটি অনুপ্রবেশ, যার শিকার ছিলেন নিরাপত্তা গবেষক Tsutomu Shimomura, এই ধরনের কৌশল (সাথে লক্ষ্যযন্ত্রকে SYN flood দিয়ে চুপ করিয়ে রাখা) বাস্তবে ব্যবহারের উদাহরণ হিসেবে পরিচিত।

প্রতিক্রিয়ায় ১৯৯৬ সালে Bellovin RFC 1948-এ প্রস্তাব দেন: ISN = ঘড়ি + (চার-মানের জোড়া আর একটি গোপন চাবির hash). ২০১২ সালে RFC 6528 এটিকে মানদণ্ড করে। চমৎকার দিক হলো, এতে মূল নির্ভরযোগ্যতার লক্ষ্যও বজায় থাকে — একই জোড়ায় নম্বর সময়ের সাথে বাড়ে, কিন্তু অন্য জোড়ার নম্বর দেখে কিছু অনুমান করা যায় না। একই যুক্তিতে IP ঠিকানা-ভিত্তিক বিশ্বাস ত্যাগ করে আজ সব জায়গায় cryptographic প্রমাণীকরণ (SSH key, TLS) ব্যবহৃত হয়।

মূল উপলব্ধি: নির্ভরযোগ্যতার জন্য বানানো একটি ঘর পরে নিরাপত্তার ঘর হয়ে উঠতে পারে। “অন্য কেউ অনুমান করতে পারবে না” — protocol-এর অনেক নিরাপত্তা আসলে এই এক বাক্যের ওপর দাঁড়িয়ে; আর ঠিকানা দেখে বিশ্বাস করা কখনোই নিরাপদ নয়।

প্র ০৩TIME-WAIT-কে অনেকে “নষ্ট সময়” মনে করেন। এটি পুরো বাদ দিলে কী ভাঙত, আর বড় সিস্টেম এর সাথে কীভাবে মানিয়ে চলে?

TIME-WAIT বাদ দিলে দুটি জিনিস ভাঙত। প্রথমত, শেষ ACK হারালে LAST-ACK-এ বসে থাকা অন্য পক্ষ FIN আবার পাঠাবে, আর এবার RST পাবে — একটি সফল আদান-প্রদানের শেষে অকারণে error. দ্বিতীয় ও বেশি গুরুতর, একই চার-মানের জোড়া সঙ্গে সঙ্গে নতুন সংযোগে ব্যবহার হলে পুরোনো সংযোগের পথে আটকে থাকা কোনো segment নতুন সংযোগের বৈধ sequence পরিসরে পড়ে চুপচাপ ডেটায় মিশে যেতে পারত — কোনো checksum এটা ধরত না, কারণ segment নিজে নির্ভুল। ভুল ডেটা নীরবে ঢোকা, নেটওয়ার্কে এর চেয়ে খারাপ খুব কমই আছে।

তবু বাস্তবে TIME-WAIT সমস্যা তৈরি করে, বিশেষ করে যেখানে এক যন্ত্র সেকেন্ডে শত শত ছোট সংযোগ খোলে-বন্ধ করে। Linux একসময় tcp_tw_recycle নামে একটি শর্টকাট রেখেছিল, যা timestamp দেখে TIME-WAIT দ্রুত মুছে ফেলত। সমস্যা দেখা দিল NAT-এর পেছনে: অনেক client একই public IP থেকে আসে, কিন্তু প্রত্যেকের timestamp-ঘড়ি আলাদা; server ভুল করে বৈধ SYN-কে পুরোনো ভেবে ফেলে দিত। মোবাইল নেটওয়ার্ক আর অফিসের NAT-এর পেছনের ব্যবহারকারীরা রহস্যময়ভাবে সংযোগ পেতেন না। শেষে Linux ৪.১২ (২০১৭)-এ option-টি সরিয়ে ফেলা হয়।

বড় সিস্টেম তাই নকশা দিয়ে সমাধান করে। HTTP/1.1 থেকে persistent connection স্বাভাবিক; HTTP/2 আর HTTP/3 একটি সংযোগেই অনেক অনুরোধ চালায় (পর্ব ৩৭)। ডেটাবেস driver আর microservice-এ connection pool. Protocol নকশাকারীরা চেষ্টা করেন যাতে client আগে বন্ধ করে, তাহলে TIME-WAIT লাখো client-এর মধ্যে ছড়িয়ে যায়। Load balancer বহু উৎস IP ব্যবহার করে চার-মানের জোড়া বাড়ায়। আর যেখানে সত্যিই দরকার, timestamp-এর সাহায্যে tcp_tw_reuse বাইরে যাওয়া সংযোগে TIME-WAIT-এর জোড়া নিরাপদে আবার ব্যবহার করতে দেয়।

মূল উপলব্ধি: একটি নিরাপত্তা-ব্যবস্থা কষ্ট দিলে সেটিকে বন্ধ করা নয়, বরং এমন নকশা খোঁজা যেখানে সেটি কম লাগে — এটাই পেশাদার সমাধান। TIME-WAIT-এর ক্ষেত্রে উত্তর: কম সংযোগ, দীর্ঘ সংযোগ।

প্র ০৪SYN cookie এত চমৎকার হলে সব সংযোগে সবসময় ব্যবহার হয় না কেন? আর SYN flood-এর ইতিহাস থেকে কী শিখি?

১৯৯৬ সালের সেপ্টেম্বরে নিউ ইয়র্কের পুরোনো ISP Panix কয়েক দিন ধরে SYN flood-এর শিকার হয়, আর এটি জনসমক্ষে আলোচিত প্রথম দিকের বড় SYN flood ঘটনাগুলোর একটি হিসেবে পরিচিত। একই সময়ে হ্যাকার-ম্যাগাজিনে কৌশলটির বিবরণ প্রকাশিত হয়েছিল, ফলে সমস্যাটি হঠাৎ সবার মাথাব্যথা হয়ে ওঠে। সেই প্রেক্ষাপটে Daniel J. Bernstein আর Eric Schenk SYN cookie-র ধারণা দেন, আর অল্প সময়ের মধ্যে Linux-এ এর বাস্তবায়ন আসে। ২০০৭ সালের RFC 4987 SYN flood আর তার প্রচলিত প্রতিরক্ষাগুলোর সারসংক্ষেপ দেয়।

সবসময় ব্যবহার না হওয়ার কারণ তথ্যের ঘাটতি। SYN-এ client যা প্রস্তাব দেয় — সঠিক MSS, window scale, SACK, timestamp — সব ৩২ bit-এ আঁটে না। মূল নকশায় MSS-কে আটটি মানের একটিতে নামাতে হয়, আর window scale ও SACK হারিয়ে যায়; ফলে লম্বা, দ্রুত পথে throughput কমে আর loss থেকে সেরে উঠতে দেরি হয়। Linux timestamp চালু থাকলে কয়েকটি option-এর তথ্য timestamp-এর নিচের কয়েক bit-এ লুকিয়ে এটা আংশিক পুষিয়ে নেয়। তাছাড়া প্রতিটি SYN-এ একটি cryptographic hash হিসাব করতে হয়, আর ফেরত ACK হারালে server আর SYN+ACK আবার পাঠাতে পারে না, কারণ তার কাছে কোনো সারিই নেই। তাই নকশাটি “জরুরি অবস্থার মোড”: queue ভরলে চালু, নইলে স্বাভাবিক SYN queue.

বড় শিক্ষা হলো আক্রমণের অসমতা। আক্রমণকারীর একটি SYN পাঠাতে খরচ প্রায় শূন্য, কিন্তু server-এর প্রতি SYN-এ মেমরি খরচ হতো। SYN cookie এই অসমতা উল্টে দেয় — অবস্থা রাখার দায় client-এর ওপর ঠেলে দেয়, যে আসল হলে ACK ফেরত দেবেই। একই চিন্তা আধুনিক protocol-এ বারবার দেখা যায়: QUIC-এর Retry token, DTLS-এর cookie, TLS-এর stateless session ticket — সবই “আগে প্রমাণ করুন আপনি এই ঠিকানায় সত্যিই আছেন, তারপর আমি মনে রাখব”।

মূল উপলব্ধি: Denial-of-service প্রতিরক্ষার মূলমন্ত্র — যাচাই না হওয়া পর্যন্ত কোনো অবস্থা না রাখা, আর আক্রমণকারীর চেয়ে নিজের খরচ কম রাখা। SYN cookie তার পাঠ্যপুস্তকের উদাহরণ।

প্র ০৫Handshake-এর এক RTT কমাতে TCP Fast Open এসেছিল, তবু ছড়াল না; QUIC ছড়াল। পার্থক্যটা কোথায়?

দুটির লক্ষ্য এক: আগে দেখা server-এ প্রথম বার্তার সাথেই ডেটা পাঠানো। TFO-র কৌশল TCP-র ভেতরেই — SYN-এ নতুন একটি option (cookie) আর ডেটা। প্রযুক্তিগতভাবে এটি কাজ করে, আর Linux বহু বছর ধরে এটি সমর্থন করে। কিন্তু ইন্টারনেটের পথে আছে লাখো middlebox — firewall, NAT, load balancer, “নিরাপত্তা” যন্ত্র — যারা TCP header খুলে দেখে। অনেকে ডেটাওয়ালা SYN বা অচেনা option দেখলে সেটি ফেলে দেয় বা option মুছে দেয়। ফলে TFO চালু করলে কিছু নেটওয়ার্কে সংযোগ ধীর হতো বা ব্যর্থই হতো, আর client-কে ফেরত-পরিকল্পনা রাখতে হতো। এর বড় অংশের ফল ছিল সীমিত গ্রহণযোগ্যতা — বড় browser-গুলো এটি ব্যাপকভাবে চালু রাখেনি বলে জানা যায়। এই ঘটনাকে বলা হয় protocol-এর ossification — ব্যবহারের চাপে protocol “জমে” যায়, নতুন কিছু যোগ করা যায় না।

QUIC এর উত্তর দিয়েছে দুই কৌশলে। প্রথম, এটি UDP-র ওপর চলে, যা middlebox-রা মোটামুটি পার করে দেয়। দ্বিতীয়, header-এর প্রায় সব কিছু encrypt করা, তাই পথের যন্ত্র ভেতরে কিছু দেখে সিদ্ধান্ত নিতে পারে না, আর ভবিষ্যতে protocol বদলালেও তারা বাধা দিতে পারে না। সাথে transport আর TLS 1.3-এর handshake একসাথে, ফলে নতুন সংযোগে এক RTT, আর আগে দেখা server-এ 0-RTT. 0-RTT-তেও TFO-র মতো replay ঝুঁকি আছে, তাই সেখানেও শুধু idempotent অনুরোধ পাঠানোর নিয়ম (বিস্তারিত পর্ব ৩৩)।

বাংলাদেশের মতো জায়গায় এর গুরুত্ব বেশি: অনেক জনপ্রিয় সেবার সার্ভার দেশের বাইরে, মোবাইল নেটওয়ার্কে RTT তুলনামূলক বেশি আর পরিবর্তনশীল। প্রতিটি RTT বাঁচানো তাই সরাসরি অনুভূত হয়। এজন্যই বড় সেবাগুলো একদিকে CDN আর দেশের ভেতরের cache ব্যবহার করে RTT ছোট করে, অন্যদিকে QUIC দিয়ে RTT-এর সংখ্যা কমায়।

মূল উপলব্ধি: ইন্টারনেটে একটি protocol কতটা ভালো, তা শুধু তার নকশা নয়, পথের পুরোনো যন্ত্রগুলো তাকে পার হতে দেয় কি না তার ওপরও নির্ভর করে। নতুন ধারণা টেকাতে হলে ছড়িয়ে পড়ার পথও নকশার অংশ।

অনুশীলন

খাতা-কলম নিয়ে বসুন — হিসাব আর পূর্বানুমানই আসল কাজ।

  1. হাতে seq/ack হিসাব: Client-এর ISN ২০০০, server-এর ৯০০০। Handshake-এর পর client ৫০০ বাইট পাঠায়, server ACK দেয়; তারপর server ১০০০ বাইট পাঠায়, client ACK দেয়। এরপর server আগে বন্ধ করে। প্রতিটি segment-এর flag, seq আর ack লিখুন। TIME-WAIT-এ কে যাবে?
     1. C→S SYN      seq=2000   ack=—
     2. S→C SYN+ACK  seq=9000   ack=2001
     3. C→S ACK      seq=2001   ack=9001
     4. C→S ডেটা 500 seq=2001   ack=9001
     5. S→C ACK      seq=9001   ack=2501
     6. S→C ডেটা 1000 seq=9001  ack=2501
     7. C→S ACK      seq=2501   ack=10001
     8. S→C FIN+ACK  seq=10001  ack=2501
     9. C→S ACK      seq=2501   ack=10002
    10. C→S FIN+ACK  seq=2501   ack=10002
    11. S→C ACK      seq=10002  ack=2502

    এই পর্বের হিসাবযন্ত্রে trace('p', 2000, 9000, ['C 500','S ACK','S 1000','C ACK','S FIN','C ACK','C FIN','S ACK']) চালালে হুবহু এই মানগুলো আসে। Server আগে FIN দিয়েছে, তাই এবার server TIME-WAIT-এ যাবে, client যাবে CLOSE-WAIT → LAST-ACK → CLOSED পথে। লক্ষ্য করুন সারি ৮: server-এর FIN নিল 10001, তাই client-এর ack 10002.

  2. Hex থেকে SYN+ACK পড়ুন: ওপরের SYN-এর উত্তরে server পাঠাল (TCP header, ৪০ বাইট): 01bb b26e 1c2d 3e4f 9a3f 21c5 a012 fe88 ce4d 0000 0204 05b4 0402 080a 7a10 c3b2 0016 a3e1 0103 0307. Port, seq, ack, header দৈর্ঘ্য, flag, window আর option বের করুন। ack মানটি কি SYN-এর সাথে মেলে?
    • Port: 01bb = 443 থেকে b26e = 45678-এ — উল্টো দিক।
    • Seq 1c2d3e4f = 472727119 — server-এর ISN.
    • Ack 9a3f21c5 = 2587828677 = SYN-এর ISN 2587828676 + 1 — মিলেছে, SYN-এর একটি নম্বর স্বীকৃত।
    • a0 → offset ১০ → ৪০ বাইট; 12 = 00010010 → ACK (0x10) + SYN (0x02).
    • Window fe88 = 65160 (SYN+ACK-এর window-ও scale হয় না); checksum ce4d; urgent 0.
    • Option: MSS 1460; SACK-permitted; timestamp TSval 7a10c3b2 (server-এর নিজের ঘড়ি), TSecr 0016a3e1 = 1483745 — client-এর TSval-এর প্রতিধ্বনি; NOP; window scale 7. দুই পক্ষই SACK, timestamp আর window scale প্রস্তাব করেছে, তাই তিনটিই চালু হবে।
  3. আউটপুট অনুমান করুন: একটি server প্রোগ্রাম client-এর সংযোগ accept() করে পড়ে, কিন্তু কোডের একটি শাখায় close() ডাকতে ভুলে যায়। Client প্রতিবার কাজ শেষে close() করে। এক ঘণ্টা পরে server-এ ss -tan চালালে কোন state বারবার দেখবেন, আর client-এ কোন state? এটা কি নিজে থেকে চলে যাবে?

    Server-এ জমবে অসংখ্য CLOSE-WAIT (প্রায়ই Recv-Q-তে ১, FIN-এর জন্য)। Client-এর দিকে সংযোগগুলো FIN-WAIT-2-এ যায়; কিন্তু client-এর socket তো close() হয়ে গেছে (orphan), তাই Linux-এ tcp_fin_timeout (ডিফল্ট ৬০ সেকেন্ড) পরে client সেগুলো নিজেই মুছে ফেলে। Server-এর CLOSE-WAIT কিন্তু কোনো timer-এ যায় না — প্রোগ্রাম close() না ডাকা পর্যন্ত, বা process শেষ না হওয়া পর্যন্ত থাকবে, file descriptor-ও আটকে থাকবে। শেষে “Too many open files”. সমাধান sysctl নয়, কোডের বাগ ঠিক করা।

  4. কমান্ড লিখুন: একটি Linux যন্ত্রে (ক) কয়টি TCP সংযোগ TIME-WAIT-এ আছে গুনুন, (খ) শুধু port 443-এর LISTEN socket-এর backlog দেখুন, (গ) ephemeral port-এর পরিসর দেখুন। তারপর হিসাব করুন: পরিসর 32768–60999 আর TIME-WAIT ৬০ সেকেন্ড হলে একই গন্তব্যে প্রতি সেকেন্ডে সর্বোচ্চ কতগুলো নতুন সংযোগ সম্ভব।
    ss -tan state time-wait | tail -n +2 | wc -l      # (ক) প্রথম শিরোনাম-লাইন বাদ
    ss -ltn 'sport = :443'                            # (খ) LISTEN-এ Send-Q = backlog, Recv-Q = accept queue
    cat /proc/sys/net/ipv4/ip_local_port_range        # (গ) যেমন: 32768  60999

    পরিসরে 60999 − 32768 + 1 = ২৮,২৩২টি port. প্রতিটি port একটি সংযোগের পর ৬০ সেকেন্ড আটকে থাকে, তাই টানা হারে ২৮২৩২ ÷ ৬০ ≈ ৪৭০টি নতুন সংযোগ প্রতি সেকেন্ডে, একই (গন্তব্য IP, port)-এর জন্য। এর বেশি দরকার হলে সংযোগ বারবার ব্যবহারই আসল সমাধান।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমClient আগে বন্ধ করলে একটি TCP সংযোগের segment-গুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. Client → Server: SYN
  2. Server → Client: SYN+ACK
  3. Client → Server: ACK (দুই পক্ষ ESTABLISHED)
  4. Client → Server: FIN (client FIN-WAIT-1)
  5. Server → Client: ACK, পরে নিজের FIN (server LAST-ACK)
  6. Client → Server: শেষ ACK (client TIME-WAIT)

Client-এর ISN 4000, server-এর ISN 9000. ডেটা ছাড়া handshake-এর তৃতীয় segment (ACK)-এ seq আর ack কত?

  • seq = 4000, ack = 9000
  • seq = 4001, ack = 9001
  • seq = 4001, ack = 4001
  • seq = 4002, ack = 9002

একটি TCP header-এর flag বাইট 0x12. এটি কোন segment?

  • শুধু SYN
  • FIN+ACK
  • SYN+ACK
  • RST+ACK

স্বাভাবিক বন্ধ করার পর TIME-WAIT অবস্থায় কোন প্রান্ত থাকে?

  • যে প্রান্ত আগে FIN পাঠিয়েছে
  • যে প্রান্ত পরে FIN পাঠিয়েছে
  • সবসময় server
  • দুই প্রান্তই সবসময়

একটি সার্ভারে ss -tan হাজার হাজার CLOSE-WAIT দেখাচ্ছে। সবচেয়ে সম্ভাব্য কারণ কী?

  • SYN flood আক্রমণ চলছে
  • TIME-WAIT-এর সময় খুব বড়
  • Receive window খুব ছোট
  • অন্য পক্ষ বন্ধ করেছে, কিন্তু সার্ভারের প্রোগ্রাম close() ডাকছে না

SYN cookie চালু থাকা একটি server SYN-এর উত্তরে কী করে?

  • RST পাঠিয়ে SYN প্রত্যাখ্যান করে
  • তথ্যগুলো নিজের ISN-এ এনকোড করে SYN+ACK পাঠায়, কোনো অবস্থা রাখে না
  • Client-এর ISN বদলে দেয়
  • SYN queue দ্বিগুণ করে

127.0.0.1-এর এমন একটি port-এ connect() করা হলো যেখানে কোনো প্রোগ্রাম শুনছে না (firewall নেই)। কী ঘটে?

  • কয়েকবার SYN পাঠানোর পর connect() timeout দেয়
  • সংযোগ তৈরি হয়, কিন্তু প্রথম read()-এ error আসে
  • Kernel RST পাঠায়, connect() পায় ECONNREFUSED
  • Client-এর socket TIME-WAIT-এ যায়

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. TCP দেয় connection-oriented, নির্ভরযোগ্য, ক্রম-ঠিক, full-duplex byte stream — ঠিক দুই প্রান্তের মধ্যে; সংযোগ মানে দুই kernel-এর হিসাবের খাতা।
  2. Header-এ port, ৩২-bit seq ও ack, data offset, ৮টি flag, window, checksum, urgent pointer আর option (MSS = MTU − ৪০, window scale, SACK, timestamp).
  3. SYN → SYN+ACK → ACK: দুই ISN পাঠানো ও স্বীকার; SYN আর FIN একটি করে নম্বর খরচ করে, খালি ACK কিছুই না; ISN এলোমেলো (RFC 6528).
  4. বন্ধ চার segment-এ; half-close (shutdown(SHUT_WR)) এক দিক খোলা রাখে; আগে বন্ধকারী TIME-WAIT-এ 2·MSL (Linux ৬০ s); সমাধান সংযোগ বারবার ব্যবহার, tw_recycle নয়।
  5. RST = এখনই বাতিল (বন্ধ port, abort, half-open); SYN queue আর accept queue; SYN flood-এর বিরুদ্ধে SYN cookie অবস্থা ISN-এ লুকায়।

পরের পর্বে: সংযোগ তো তৈরি হলো — এবার ডেটা চলার সময় TCP কীভাবে নিশ্চিত করে কিছু হারাবে না। প্রতিটি বাইটের sequence number, cumulative ACK, হারানো segment আবার পাঠানো, আর কতক্ষণ অপেক্ষা করে আবার পাঠাতে হবে (RTO) তার হিসাব।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান