Foundationপ্রথম নীতি থেকে
LEVEL 7লেসন ১১/২২অ্যাডভান্সড১ ঘণ্টা

TCP — three-way handshake, state machine, seq/ack

TCP Handshake and State Machine

একটা connection জন্ম নেয়-মরে কীভাবে — SYN-এ শুরু, TIME_WAIT-এ শেষ; পুরো TCP state machine-কে packet-by-packet ধরে অনুসরণ, আর 'কেন ঠিক তিনটা ধাপ' প্রশ্নের ৪০ বছরের উত্তর।

এই লেসন শেষে আপনি পারবেন

  • TCP segment header-র প্রতিটা প্রধান field — port, 32-bit seq, 32-bit ack, flag bitfield (SYN/ACK/FIN/RST/PSH/URG), window — hexdump-এ দেখে চিনতে ও ভূমিকা ব্যাখ্যা করতে পারবেন
  • Three-way handshake-এর প্রতিটা message-এ ঠিক কোন field কী মান বহন করে trace করে, কেন দুই-way ঐতিহাসিকভাবে ব্যর্থ হয়েছিল — তা যুক্তি দিয়ে বলতে পারবেন
  • ISN (Initial Sequence Number) randomization-এর security কারণ — blind spoofing attack-এর সাথে সম্পর্ক — ব্যাখ্যা করতে পারবেন
  • LISTEN/SYN_SENT/SYN_RECV/ESTABLISHED/FIN_WAIT/TIME_WAIT/CLOSE_WAIT state diagram-টা আঁকতে পারবেন, আর TIME_WAIT ঠিক 2*MSL কেন — দুইটা আলাদা কারণসহ বলতে পারবেন
  • 4-way teardown আর simultaneous close-এর packet sequence হাতে আঁকতে পারবেন, আর backlog/accept queue OS-পর্যায়ে কী ঘটে তার একটা সংক্ষিপ্ত map বলতে পারবেন

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

UDP লেসনে আমরা দেখেছি ডাকপত্রের জগৎ — ঠিকানা লিখে ছুড়ে দিলেন, বাকিটা ভাগ্য। আজ টেলিফোনের জগৎ: কথা বলার আগে উভয় পক্ষকে জানতে হয় লাইন খোলা আছে কি না, কারা কথা বলছে, কোথা থেকে শুরু হচ্ছে।

কিন্তু TCP-র “hello” চেয়ে অনেক বেশি চতুর। কারণ TCP যে promise দেয় — ক্রমবিন্যস্ত, নির্ভরযোগ্য byte-stream — সেই promise রাখতে হলে সংযোগ শুরুর আগেই দুই পক্ষের shared secret লাগবে: প্রত্যেকে জানবে অন্য পক্ষ কোন byte-number থেকে গুনতে শুরু করছে। এই সংখ্যাটার নাম ISN — Initial Sequence Number, আর handshake-এর আসল কাজ এই ISN exchange আর mutual confirmation।

আজকের follow-the-packet journey:

  • SYN → SYN-ACK → ACK: তিনটা খালি packet-এর ভেতরে ঠিক কী কী লেখা থাকে
  • কেন ঠিক তিনটা — দুইটা কেন ১৯৭০-এর দশকে ভেঙেছিল
  • ISN কেন আজ র‍্যান্ডম — একটা বিখ্যাত hack-এর গল্প
  • পুরো জীবনচক্র: LISTEN-এ জন্ম, ESTABLISHED-এ বিকাশ, FIN-এ বিদায় — আর TIME_WAIT-এ ৪ মিনিটের মৃত্যুপরবর্তী জীবন
  • ss output-এ এই সব state নিজের চোখে দেখা

মূল ধারণা

TCP segment header — যে fields আজ দরকার

পূর্ণ header 20–60 byte; আজকের লেসনের নায়করা:

FieldSizeকাজ
Source / Dest Port2+2UDP-র মতোই application ঠিকানা
Sequence Number4এই segment-এর প্রথম byte-এর স্ট্রিম-অবস্থান
Acknowledgment Number4যা পেয়েছি তার পরের expected byte
Flags~1 byte bitfieldSYN, ACK, FIN, RST, PSH, URG
Window2flow control advertisement (লেসন ১২-১৩)
Checksum2pseudo-header সহ (UDP লেসনের মতো)
Options0–40MSS, window scale, SACK-permitted…

Flags-এর প্রতিটা bit একটা বাক্য:

  • SYN — “synchronize: আমি X থেকে গুনা শুরু করছি”
  • ACK — “acknowledgment field valid — আমি তোমার Y পর্যন্ত পেয়েছি” (সংযোগ তৈরির পরে প্রতিটা segment-এ সেট)
  • FIN — “আমার পাঠানো শেষ” (polite half-close)
  • RST — “এই সংযোগ অস্বীকৃত/ভাঙো — এখনই” (rude abort)
  • PSH — “receiver-এ buffer না জমিয়ে app-কে এখনই দাও”
  • URG — urgent pointer valid (আজ মৃতপ্রায়)

Three-way handshake — packet by packet

Client A, server B; B আগে listen()-এ বসে আছে:

A                                    B
|---- SYN, seq=100 ----------------->|     "আমি 101 থেকে data পাঠাব,
|                                    |      তুমি যা পাঠাবে তা ACK করো"
|
|<-- SYN+ACK, seq=300, ack=101 ------|     "আমি 301 থেকে গুনব; তোমার
|                                    |      100 পেয়েছি — পরেরটা 101"
|
|---- ACK, ack=301 ----------------->|     "তোমার 300 পেয়েছি — পরেরটা 301"

তিনটা মুহূর্তে তিনটা ঘটনা:

  1. SYN: client নিজের ISN (100) ঘোষণা করে
  2. SYN+ACK: server একসাথে client-এর ISN acknowledge করে (ack=101) এবং নিজের ISN (300) ঘোষণা করে — দুই কাজ এক packet-এ, এজন্যই “three”-way, চার নয়
  3. ACK: client server-এর ISN confirm করে — এখন দুই দিকেই দুইটা গুনতে-শুরুর-সংখ্যা জানা

কেন ঠিক তিন? দুই-way-এর historical failure

ধরা যাক দুই-way: A পাঠায় SYN, B উত্তরে SYN-ACK দিলেই connection open। সমস্যা: একটা পুরনো, বিলম্বিত duplicate SYN network-এ ঘুরেফিরে দুই দিন পরে B-তে পৌঁছাল। B ভাববে নতুন connection- এর request — open করে ফেলবে, আর বসে থাকবে এমন client-এর জন্য যার অস্তিত্বই নেই। Resource leak, ghost connection।

তিন-ধাপে এটা বন্ধ হয়: connection তৈরির জন্য B-কে দরকার client- এর live proof — ঠিক ওই ISN-এর উত্তরে একটা ACK, যা একটা dead duplicate SYN কখনো বহন করে না। অর্থাৎ তৃতীয় ধাপটা শুধু formality নয় — এটা B-র সুরক্ষা। RFC 793-এর ভাষায়ই rationale: দুই-way-এ পুরনো duplicate SYN থেকে অর্ধ-খোলা connection-এর ঝুঁকি।

ISN randomness — নিরাপত্তার পাঠ

প্রথম দিনে ISN ছিল সরল বাড়ন্ত ঘড়ি — প্রতি সেকেন্ডে নির্দিষ্ট হারে বাড়ত। ফলাফল: অন্য কেউ আপনার পরের ISN অনুমান করতে পারত, তারপর victim-এর ঠিকানা জালিয়ে বৈধ SYN-ACK-এর উত্তরে ACK পাঠিয়ে connection hijack করতে পারত (blind spoofing — 1985-এর Morrris paper, আর বিখ্যাত Mitnick attack 1995-এ ঠিক এটাই করেছিল)।

আজকের নিয়ম (RFC 6528): ISN = clock + cryptographic hash (per-connection secret) — অনুমানযোগ্য নয়। মনে রাখবেন ISN-randomness confidentiality দেয় না, শুধু off-path attacker-কে অনুমান অসম্ভব করে।

State machine — পুরো জীবনচক্র

Server-side ও client-side রাজ্যপথ
  1. LISTENserver bind+listen-এ বসে আছে
  2. SYN_SENTclient SYN পাঠিয়েছে, উত্তর অপেক্ষায়
  3. SYN_RECVserver SYN পেয়ে SYN-ACK পাঠিয়েছে
  4. ESTABLISHEDউভয় পক্ষ ready — data চলতে পারে
  5. FIN_WAIT_1 / FIN_WAIT_2আগে-বন্ধকারী FIN পাঠিয়ে উত্তরের অপেক্ষায়
  6. TIME_WAITশেষ ACK পাঠানোর পরে 2*MSL অপেক্ষা
  7. CLOSE_WAITপরে-বন্ধকারীর: peer-এর FIN পেয়েছে, নিজের FIN পাঠায়নি
  8. LAST_ACK → CLOSEDপরে-বন্ধকারীর শেষ অধ্যায়

TIME_WAIT — কেন মরে না সরাসরি

যে পক্ষ আগে close শুরু করে (FIN পাঠায় আগে), শেষ ACK পাঠানোর পরে সে TIME_WAIT-এ ঢোকে — ঠিক 2×MSL (Maximum Segment Lifetime, typical 30s–2min) সময়ের জন্য। কারণ দুইটা, দুটোই আলাদা:

  1. Dangling final ACK: শেষ ACK হারালে peer তার FIN retransmit করবে — TIME_WAIT-এ থাকলে আমরা সেই retransmit-এর উত্তরে আবার ACK পাঠাতে পারি। এখনই বন্ধ হয়ে গেলে peer RST পেত — graceful close-এর পরিবর্তে error।
  2. পুরনো duplicate segment মৃত্যুদণ্ড: এই connection-এর দেরিতে-আসা পুরনো segment-গুলো network-এ ২×MSL-এর মধ্যে সব মরে যায়। এরপর একই 4-tuple-এ নতুন connection খুললে সে পুরনো ghost data পাবে না।

Teardown — ৪-way এবং simultaneous close

Connection ভাঙতে প্রতিটা দিক আলাদাভাবে বন্ধ হয় (half-close সম্ভব — এক পক্ষ data পাঠানো বন্ধ করেও পড়তে পারে):

A                                      B
|---- FIN, seq=X --------------------->|     "আমার শেষ"
|<--- ACK, ack=X+1 --------------------|     "জেনেছি"   (B-এর data এখনো চলতে পারে!)
|<--- FIN, seq=Y ----------------------|     "B-ও শেষ"
|---- ACK, ack=Y+1 ------------------->|     "জেনেছি"   → A: TIME_WAIT

চার ধাপ, কারণ ACK আর FIN মিলে যাওয়ার নিশ্চয়তা নেই — B-র application হয়তো শেষ data-গুলো এখনো পাঠাচ্ছে। Simultaneous close (দুই পক্ষ একসাথে FIN পাঠায়): দুই দিকই FIN_SENT→FIN_WAIT_2 পথ এড়িয়ে CLOSING state দিয়ে যায় — বৈধ, বিরল, ss-তে দেখা যায়।

Backlog — kernel-এর অপেক্ষার ঘর

listen(fd, backlog)-এর সেই backlog সংখ্যাটা OS-পর্যায়ে দুইটা queue নিয়ন্ত্রণ করে: SYN queue (SYN পেয়েছে, ACK আসেনি — SYN_RECV) আর accept queue (fully established, কিন্তু app-এর accept() ডাকেনি)। Application busy থাকলে accept queue ভরে; overflow হলে Linux সাধারণত incoming ACK ফেলে দেয়। SYN flood- এর বিরুদ্ধে syncookies এই SYN queue-কেই bypass করে। এই পুরো ব্যবস্থার event-driven দিক — select/poll/epoll — operating-systems module-এর io-models লেসনে গভীরভাবে আসবে; এখানে শুধু map-টা রাখুন।

ভেতরে কী ঘটছে

ISN — শূন্য থেকে গোনা কেন বন্ধ

Sequence number-এর initial value (ISN) randomize করা হয়। পুরনো implementation-গুলো ISN=0 বা clock-based predictable মান দিত — আর সেটা একটা ভয়ংকর attack-এর দরজা খুলে দিত: attacker আপনার ISN predict করতে পারলে, চলমান connection-এ ভুয়া segment inject করতে পারে (seq number ঠিক জানা লাগে, password না)। ARPANET-যুগে 1985-এ Morris-এর paper-ই এই technique document করেছিল; আধুনিক OS প্রতি connection-এ cryptographic-random ISN দেয় ঠিক এই কারণে।

আরেকটা subtlety: sequence number field মাত্র ৩২ bit — 4GB data পাঠালেই গুণতা ঘুরে (wrap) আবার শুরু হয়। TCP-র গণিত তাই modulo arithmetic: “নতুন” মানে পুরনোর থেকে 2^31-এর মধ্যে সামনে (serial number arithmetic)। এই ধারণা পরে SCTP/QUIC-ও উত্তরাধিকার নিয়েছে।

TIME_WAIT — অপেক্ষাটা কার জন্য

Connection বন্ধের শেষে active-closer দুই minute (২×MSL) পর্যন্ত TIME_WAIT-এ বসে থাকে — নতুন developer-দের কাছে এটা bug মনে হয়, আসলে দুইটা নিরাপত্তা:

১. শেষ ACK হারালে peer তার FIN retransmit করবে। TIME_WAIT না থাকলে ওই retransmit-এর reply হতো RST — peer-এর কাছে clean close-এর বদলে error দেখাত। থাকলে ঠিক ACK-টা আবার পাঠাতে পারি।

২. পুরনো connection-এর বিলম্বিত duplicate segment network- এ ঘুরে ফিরতে পারে (MSL = সর্বোচ্চ segment lifetime)। একই 4-tuple দিয়ে সঙ্গে সঙ্গে নতুন connection খুললে ওই ghost segment নতুন connection-এর data হিসেবে ঢুকে যেত — corruption। ২×MSL অপেক্ষা করলে সব duplicate পথেই মারা যায়।

SYN flood আর syncookies

Half-open connection (SYN এসেছে, ACK আসেনি) প্রতিটা kernel-এ state জমায় — SYN queue-র একটা entry। Attacker spoofed source address দিয়ে হাজার হাজার SYN পাঠালে queue ভরে যায়, বৈধ user connect করতে পারে না। Syncookies এর প্রতিষেধ: SYN-ACK-এর ISN-এর ভেতরে connection state-টাই encode করে পাঠানো হয় (source/ dest/port/MSS-এর hash + counter) — kernel SYN queue-তে কিছুই রাখে না। Final ACK এলে hash verify করে state reconstruct করে। Trade-off: TCP options (window scale, SACK) পুরোটা fit হয় না — তাই syncookies শুধু overflow-এর সময় activate হয়, স্বাভাবিক অবস্থায় না।

Normal:   SYN ──▶ [queue-তে entry] ──▶ SYN-ACK ──▶ ACK ──▶ established
Cookie:   SYN ──▶ [queue খালি!] ────▶ SYN-ACK(ISN=hash) ──▶ ACK(verify) ──▶ established

উদাহরণ

একটা সম্পূর্ণ জীবন — জন্ম, data, মৃত্যু

Concrete numbers সহ পুরো trace (client ISN=1000, server ISN=5000):

# --- জন্ম ---
C→S  [SYN]        seq=1000
S→C  [SYN,ACK]    seq=5000, ack=1001
C→S  [ACK]        ack=5001                  # ESTABLISHED দুই পক্ষে

# --- data (client→server) ---
C→S  [ACK,PSH]    seq=1001, len=10          # bytes 1001..1010
S→C  [ACK]        ack=1011                   # "পরেরটা 1011 থেকে"

# --- মৃত্যু (client আগে বন্ধ করল) ---
C→S  [FIN,ACK]    seq=1011                   # FIN-ও ১ byte খায়!
S→C  [ACK]        ack=1012                   # C: FIN_WAIT_2
S→C  [FIN,ACK]    seq=5001                   # S-এর নিজের শেষ
C→S  [ACK]        ack=5002                   # S: CLOSED; C: TIME_WAIT

লক্ষ্য করুন তিনটা সূক্ষ্ম বিষয়:

  • SYN-এর পরে প্রথম data byte seq=1001 — SYN নিজে 1000 নিয়েছে
  • Server-এর ACK (1012)-এর পরেও server নিজের data পাঠাতেই পারে — half-close-এর সুবিধা
  • Client-এর শেষ ACK-এর পরেই সে TIME_WAIT-এ — আগে-বন্ধকারীই এই অপেক্ষার দায় বহন করে

ss-এ এই সব state দেখা

ss -tan state time-wait          # শুধু TIME_WAIT
ss -tan                          # সব TCP socket
State       Recv-Q  Send-Q  Local Address:Port   Peer Address:Port
ESTAB            0       0  192.168.1.20:52344   142.250.77.14:443
LISTEN           0     128        0.0.0.0:22             0.0.0.0:*
TIME-WAIT        0       0  127.0.0.1:52400        127.0.0.1:8080

প্রতিটা লাইন একটা socket, প্রতিটা state আমাদের state-machine- এর একটা নোড। Recv-Q/Send-Q non-zero মানে data kernel buffer-এ অপেক্ষমাণ — sockets-api লেসনে এটাই মূল বিষয়।

ছবিতে

State transition map

                 active open (client)              passive open (server)
                ┌──────────────┐                    ┌──────────────┐
                │   CLOSED     │                    │    CLOSED    │
                └──┬───────────┘                    └───────┬──────┘
              send SYN                              listen()
                ▼                                          ▼
           ┌─────────┐  recv SYN                     ┌─────────┐
           │SYN_SENT │◄──────────────────────────────│ LISTEN  │
           └────┬────┘                               └────┬────┘
        recv SYN|ACK                                 recv SYN
          send ACK                                   send SYN|ACK
                ▼                                          ▼
                └────────► ESTABLISHED ◄───────────────────┘
                                │
                        close(): send FIN
                                ▼
                          FIN_WAIT_1 ──(simultaneous FIN)──► CLOSING
                                │recv ACK                         │recv ACK
                                ▼                                  │
                           FIN_WAIT_2 ◄────────────────────────────┘
                                │recv FIN, send ACK
                                ▼
                            TIME_WAIT ──(2*MSL)──► CLOSED

  passive-close পথ:  ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
                      (peer-এর FIN পেয়ে)  (নিজের FIN)  (ACK পেয়ে)
TCP connection-এর পূর্ণ state diagram — বাঁ পথ client (active open), ডান পথ server (passive open); নিচে মিলে ESTABLISHED, আবার আলাদা হয়ে দুই রকম মৃত্যু।

আর handshake-এ দুই পক্ষের সমসাময়িক অবস্থা:

Wire:      C ──── SYN ────────────► S
States:  SYN_SENT                 SYN_RECV

Wire:      C ◄─── SYN+ACK ────────  S
States:  ESTABLISHED              SYN_RECV

Wire:      C ──── ACK ────────────► S
States:  ESTABLISHED              ESTABLISHED

ACK হারালে? C: ESTABLISHED (ভুল ধারণায়!)   S: SYN_RECV, FIN-ACK
retransmit করতেই থাকে → timeout-এ C-র প্রথম data-segment-এর
ACK-ই কাজ সারাবে (handshake-এর তৃতীয় ACK কার্যত optional-ish)
প্রতিটা wire-message দুই endpoint-এর state-কে একসাথে এগিয়ে দেয় — একটা মাত্র lost ACK-ই গল্পটা বদলে দেয়।

নিজে চালিয়ে দেখুন

EXPERIMENT

state দেখা, capture পড়া, আর TIME_WAIT-এর স্তূপ বানানো

macOS / Linux· ২০ মিনিট

ধাপ ১ — connect/disconnect loop-এ state watch:

এক terminal-এ ছোট listener চালু করুন:

python3 -c "import socket,time; s=socket.socket(); s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1); s.bind(('127.0.0.1',9099)); s.listen(5);
while True: c,a=s.accept(); c.close(); print('conn from',a)"

আরেক terminal-এ দ্রুত বারবার connect করুন:

while true; do nc -z 127.0.0.1 9099; sleep 0.2; done

তৃতীয় terminal-এ live দেখুন (Linux):

watch -n0.3 "ss -tan | grep -E '9099'"
# macOS-এ watch না থাকলে: while :; do clear; netstat -an -ptcp | grep 9099; sleep 0.3; done

ESTABLISHED-এর ঝলক, তার পরে TIME_WAIT জমা — সব live।

ধাপ ২ — handshake capture পড়ুন (sudo লাগে):

sudo tcpdump -i any -n -S 'tcp port 9099'    # -S: absolute seq

Capture-এ এই প্যাটার্ন খুঁজুন (annotation সহ):

IP .52344 > .9099: Flags [S],  seq 2847391000        ← client SYN
IP .9099 > .52344: Flags [S.], seq 1990002000,
                   ack 2847391001                     ← SYN+ACK (ack=ISN+1!)
IP .52344 > .9099: Flags [.],  ack 1990002001         ← final ACK
IP .52344 > .9099: Flags [F.], seq 2847391001         ← FIN (nc বন্ধ করল)
IP .9099 > .52344: Flags [F.], seq 1990002001         ← server-ও FIN
IP .52344 > .9099: Flags [.],  ack 1990002002         ← শেষ ACK

[S.], [F.] মানে SYN+ACK, FIN+ACK — flag bitfield-এর দুই bit। ack সবসময় peer-এর seq+1 — SYN/FIN-ও এক-এক byte।

ধাপ ৩ — TIME_WAIT pileup reproduction:

for i in $(seq 1 40); do curl -s -o /dev/null http://127.0.0.1:9099 || \
  python3 -c "import socket; s=socket.socket(); s.connect(('127.0.0.1',9099)); s.close()" ; done
ss -tan state time-wait | wc -l        # macOS: netstat -an | grep -c TIME_WAIT

curl প্রতিবার আগে বন্ধ করে — তাই 40-এর কাছাকাছি TIME_WAIT client-side-এ জমবে। এক-একটা ~60 সেকেন্ড (macOS default 15s, Linux 60s) পরে নিজে থেকেই মুছে যাবে — 2×MSL-এর বাস্তব রূপ। এই স্তূপই high-concurrency server-এ SO_REUSEADDR-এর প্রয়োজন — sockets-api লেসনে সেটা বিস্তারিত।

এটা কী প্রমাণ করে

connection-এর জীবনের প্রতিটা মুহূর্তে kernel ঠিক যে state-ই বলা হয়েছে সেটাতেই থাকে, আর rapid connect/disconnect-এ TIME_WAIT শুধু আগে-বন্ধকারীর পায়েই জমে — theory আর ss output হুবহু মিলে যায়।

নিজে বানান

BUILD IT

TCP state-machine tracker — event log থেকে transition ও violation

Python · ●●○○○
  1. pcap-সদৃশ text log পড়ুন: প্রতি লাইনে t, endpoints, flags
  2. প্রতিটা 4-tuple-এর জন্য state রাখুন dict-এ — দুই পক্ষ আলাদা state-এ
  3. Legal-transition table define করুন — বৈধ পথের বাইরে গেলে VIOLATION ছাপুন
  4. TIME_WAIT entry/exit accounting রাখুন — 2*MSL-এর বাস্তব simulation
  5. শেষে summary: মোট connection, violations, orphan states

লক্ষ্য: kernel ভেতরে যা করে, সেই state tracking-এর মূলটা নিজের হাতে লেখা — একটা hand-provided event log consume করে transitions print করা, বৈধ-অবৈধ ধরা, আর TIME_WAIT হিসাব রাখা।

from collections import defaultdict

MSL = 30.0                       # seconds — textbook value

# প্রতিটা wire-event দুই endpoint-কেই ছোঁয় — sender-এর উপর একটা
# প্রভাব, receiver-এর উপর আরেকটা। টেবিলের মান: {current: next};
# খালি dict মানে "এই দিকে state বদলায় না" (বৈধ no-op)
T = {
    ("send", "SYN"):    {"CLOSED": "SYN_SENT"},
    ("send", "SYNACK"): {},
    ("send", "ACK"):    {"ESTABLISHED": None},
    ("send", "FIN"):    {"ESTABLISHED": "FIN_WAIT_1",
                         "CLOSE_WAIT":  "LAST_ACK"},
    ("send", "FINACK"): {},
    ("recv", "SYN"):    {"LISTEN": "SYN_RECV"},
    ("recv", "SYNACK"): {"SYN_SENT": "ESTABLISHED"},
    ("recv", "ACK"):    {"SYN_RECV": "ESTABLISHED"},
    ("recv", "FIN"):    {"ESTABLISHED": "CLOSE_WAIT",
                         "FIN_WAIT_2":  "TIME_WAIT"},
    ("recv", "FINACK"): {"FIN_WAIT_1": "FIN_WAIT_2",
                         "LAST_ACK":   "CLOSED"},
}

def parse(line):
    """'t=0.001 C->S SYN' জাতীয় লাইন -> (t, src, dst, event)"""
    toks = line.split()
    t = float(toks[0].split("=")[1])
    src, dst = toks[1].split("->")
    return t, src, dst, toks[2]

def track(log):
    state = defaultdict(lambda: {"C": "CLOSED", "S": "LISTEN"})
    tw_since = {}
    for line in log:
        t, src, dst, ev = parse(line)
        key = tuple(sorted((src, dst)))
        snd = "C" if key[0] == src else "S"
        rcv = "S" if snd == "C" else "C"
        notes = []
        for side, action in ((snd, ("send", ev)), (rcv, ("recv", ev))):
            cur = state[key][side]
            table = T.get(action, {})
            if cur not in table and table:
                print(f"{line:<26} VIOLATION: {side}-side "
                      f"{cur} + {action[0]}-{action[1]} অবৈধ!")
                continue
            nxt = table.get(cur)
            if nxt is None:
                continue                       # বৈধ no-op
            state[key][side] = nxt
            if nxt == "TIME_WAIT":
                tw_since[key] = t
                notes.append(f"{side}: TIME_WAIT (2*MSL={2*MSL:.0f}s)")
            elif nxt == "CLOSED":
                notes.append(f"{side}: LAST_ACK→CLOSED")
            else:
                notes.append(f"{side}: {cur}→{nxt}")
        if notes:
            print(f"{line:<26} " + "; ".join(notes))
    for k, v in state.items():
        stuck = [s for s in v.values() if s not in ("CLOSED", "LISTEN")]
        if stuck:
            print(f"\nORPHAN {k}: {v}")

LOG = [
    "t=0.000 C->S SYN",           # active open
    "t=0.010 S->C SYNACK",
    "t=0.012 C->S ACK",
    "t=1.500 C->S FIN",           # client আগে বন্ধ করল
    "t=1.505 S->C FINACK",
    "t=1.600 S->C FIN",
    "t=1.610 C->S FINACK",        # server CLOSED; client TIME_WAIT-এ
    "t=5.000 C->S SYN",           # জালিয়াতি: TIME_WAIT থেকেই নতুন SYN!
]
track(LOG)

প্রত্যাশিত output:

t=0.000 C->S SYN           C: CLOSED→SYN_SENT; S: LISTEN→SYN_RECV
t=0.010 S->C SYNACK        C: SYN_SENT→ESTABLISHED
t=0.012 C->S ACK           S: SYN_RECV→ESTABLISHED
t=1.500 C->S FIN           C: ESTABLISHED→FIN_WAIT_1; S: ESTABLISHED→CLOSE_WAIT
t=1.505 S->C FINACK        C: FIN_WAIT_1→FIN_WAIT_2
t=1.600 S->C FIN           C: FIN_WAIT_2→TIME_WAIT (2*MSL=60s); S: CLOSE_WAIT→LAST_ACK
t=1.610 C->S FINACK        S: LAST_ACK→CLOSED
t=5.000 C->S SYN           VIOLATION: C-side TIME_WAIT + send-SYN অবৈধ!
t=5.000 C->S SYN           VIOLATION: S-side CLOSED + recv-SYN অবৈধ!

ORPHAN ('C', 'S'): {'C': 'TIME_WAIT', 'S': 'CLOSED'}

শেষ অংশই মূল শিক্ষা — TIME_WAIT-এর ভেতর থেকে নতুন connection বৈধ নয় (tracker ধরে ফেলল), আর log-এ অসম্পূর্ণ রেখে যাওয়া connection orphan report-এ চিৎকার করে ওঠে।

নিজে বাড়ান:

  1. Event-ভিত্তিক dual lookup যোগ করুন — SYNACK যেন client-table-এ যায় আর SYN server-table-এ (উপরের সূক্ষ্মতাটা ঠিক করুন)
  2. t দেখে TIME_WAIT-এর 2×MSL expiry স্বয়ংক্রিয়ভাবে fire করুন
  3. Simultaneous close (দুই দিকেই FIN) detect করে CLOSING state যোগ করুন

বাস্তব সিস্টেমে

বাস্তব জগতে handshake আর state

SYN flood আর syncookies। Attacker জাল source address দিয়ে হাজার হাজার SYN পাঠায় — server-এর SYN queue SYN_RECV-তে ভরে যায়, বৈধ user ঢুকতে পারে না। Linux-এর syncookies: SYN queue bypass করে server-এর উত্তরের ভেতরেই cryptographic state লুকিয়ে পাঠায় — memory না খেয়ে handshake verify। 1996-এর পুরনো আইডিয়া, আজও production-এ।

TIME_WAIT exhaustion। Load-test-এ হঠাৎ “Cannot assign requested address” — ephemeral port শেষ, কারণ হাজার ছোট connection-এর TIME_WAIT জমছে। Production fix: connection pooling/keep-alive (HTTP-র Connection: keep-alive ঠিক এই খরচ বাঁচায়), SO_REUSEADDR, বা tcp_tw_reuse।

RST আর port scanning। nmap-এর connect scan-এর প্রতিটা উত্তর আসলে state machine-এর ফাঁদ: খোলা port-এ SYN-এর উত্তর SYN-ACK, বন্ধ port-এ RST — scanner শুধু flag পড়েই service-এর অস্তিত্ব জানে। Firewall “stealth” (drop) বনাম reject (RST)-এর তর্কটাও এখান থেকে।

TCP hole punching NAT-এ। দুই NAT-এর পেছনের peer-রা একসাথে একে অপরের ঠিকানায় SYN পাঠালে (simultaneous open!) — দুই NAT-এই mapping তৈরি হয়ে direct P2P connection দাঁড়ায়। State machine- এর এক বিরল পথ যেভাবে WebRTC-র ভিত্তি হয়েছে।

Half-close-এর বাস্তব ব্যবহার। HTTP/1.0-যুগের upload: client request-body-র শেষে shutdown(SHUT_WR) করত — “আমি পাঠালাম, তুমি পড়ো”; server full response পাঠাতে পারত। আজও কিছু CLI tool এই পথেই চলে।

যে ভুলগুলো সবাই করে

“Server SYN-ACK পাঠানোর সাথে সাথেই connection established।”

Server-পাশে state তখন SYN_RECV — established না! Final ACK আসার আগে server data-র ভরসা করতে পারে না। ss-তে SYN_RECV-এর লাইন দেখলে বুঝবেন handshake অসম্পূর্ণ — হয় ধীর client, হয় SYN flood। আর client-পাশে কিন্তু SYN+ACK পেলেই ESTABLISHED — এই asymmetry-ই প্রমাণ “established” দুই পক্ষে একসাথে ঘটে না।

“TIME_WAIT একটা bug বা waste — বাদ দেওয়া উচিত।”

TIME_WAIT protocol-এর নিরাপত্তা-জাল: হারানো শেষ ACK-র জন্য second-chance, আর পুরনো duplicate segment-গুলোকে নতুন connection দূষিত করা থেকে বাঁচানো। এটা বাদ দিলে একই 4-tuple-এ তাৎক্ষণিক নতুন connection-এ পুরনো বিলম্বিত data ঢুকে যেতে পারত — নীরব, ভয়ংকর corruption। সমস্যা হলে সমাধান pooling/reuse, state-টা অক্ষত নয়।

“Three-way handshake-এ data পাঠানো যায় না, আর তিন ধাপ শুধু আনুষ্ঠানিকতা।”

দুই ভাগে আধা-সত্য। TFO (TCP Fast Open) SYN-এর সাথে data বহন করে — cookie-র বিনিময়ে; বিরল কিন্তু standard-এ আছে। আর তৃতীয় ধাপ formal নয় — দুই-way-এর historical duplicate-SYN failure-ই তার প্রমাণ: তৃতীয় ACK ছাড়া server-এর কাছে client-এর liveness- এর কোনো proof থাকে না।

“RST পাওয়া মানে normal বন্ধ — FIN-এর মতোই একটা close।”

FIN হলো বিদায়-চিঠি: ordered, সব pending data flush, state machine মেনে। RST হলো দরজা বাজিয়ে ভেঙে ফেলা: সব buffered data ফেলে দিয়ে সঙ্গে সঙ্গে ধ্বংস — কোনো TIME_WAIT-ও না। App-level এটা সাধারণত error হিসেবেই পৌঁছায় (“Connection reset by peer”)। দুইটা কোনোভাবেই interchangeable নয়।

বুঝেছেন কি না দেখুন

1

Handshake-এর তিনটা message-এ মোট কয়টা distinct তথ্য exchange হয়? প্রতিটার নাম বলুন।

স্মরণ

চারটা: (১) client-এর ISN ঘোষণা (SYN-এ), (২) client সেটার জন্য server-এর ACK, (৩) server-এর ISN ঘোষণা (SYN-ACK-এ), (৪) server সেটার জন্য client-এর ACK। মাঝের দুটো এক packet (SYN+ACK) ভাগ করে নেয় বলেই মোট তিনটা packet — কিন্তু চারটা তথ্য।

2

Client-এর ISN = 8000। SYN-এর পরে প্রথম data segment-এ যদি 100 byte থাকে, তার seq কত? Server-এর ACK-এর ack value কত?

প্রয়োগ

SYN নিজে 8000 নেয় — তাই প্রথম data byte seq = 8001। 100 byte মানে bytes 8001..8100 — server ACK করবে 8101 (“পরের expected”)। Ack সবসময় সর্বশেষ received-এর এক পরে — cumulative semantics-এর ভিত্তি, পরের লেসনের মূল অস্ত্র।

3

দুই-way handshake-এ (SYN → SYN-ACK, শেষ!) পুরনো duplicate SYN কী সমস্যা করত? তৃতীয় ACK ঠিক কোন প্রশ্নের উত্তর?

যুক্তি

Duplicate SYN দুই দিন বিলম্বিত হয়ে server-এ পৌঁছালে server নতুন connection ভেবে resource allocate করত — আর client কখনো আসবেই না; ghost connection জমত। তৃতীয় ACK-র প্রশ্নটা server-এর দৃষ্টিতে: “তুমি এই মুহূর্তে সত্যিই জীবিত এবং এই ISN-এর উত্তর দিতে সক্ষম?” — একটা stale SYN কখনো এই fresh ACK বহন করতে পারে না।

4

TIME_WAIT কেন ঠিক 2×MSL — 1×MSL কেন নয়? দুইটা আলাদা কাজের কথা বলুন।

যুক্তি

(১) Dangling final-ACK: শেষ ACK হারালে peer FIN retransmit করে — সেই retransmit আসতে এক MSL লাগতে পারে, উত্তর দিতে আরেক MSL — মোট 2×MSL-এর জানালা। (২) Ghost segments: এই connection- এর পুরনো বিলম্বিত segment-গুলো network-এ সর্বোচ্চ এক MSL বাঁচে; একটা MSL অতিরিক্ত অপেক্ষায় নিশ্চিত হয় একই 4-tuple-এ নতুন connection কখনো পুরনো data পাবে না।

5

Server আগে বন্ধ করলে (server FIN পাঠায় আগে) — TIME_WAIT কার পায়ে জমবে, server না client? এটা load-balanced web-server-এ কেন সমস্যা হতে পারে?

প্রয়োগ

যে আগে FIN পাঠায় সে-ই শেষ ACK-র পরে TIME_WAIT-এ যায় — এখানে server। Web-server হাজার ছোট response connection নিজেই বন্ধ করলে তার পায়েই লক্ষ TIME_WAIT জমে — port/memory চাপ। তাই অনেক server নিয়ম রাখে client-কেই আগে বন্ধ করাবে (HTTP keep-alive এরই এক রূপ), বা SO_REUSEADDR/tcp_tw_reuse ব্যবহার করে।

6

আপনার proxy-র log-এ দেখছেন একই client-IP-তে সেকেন্ডে হাজার SYN, কিন্তু কোনো final ACK নেই — SYN_RECV জমছে। কী ঘটছে এবং kernel-level দুইটা mitigation বলুন।

ডিজাইন

এটা SYN flood — জাল source address-এর SYN-গুলোর final ACK কখনো আসবে না, SYN queue ভরে SYN_RECV-তে। Mitigation: (১) syncookies — SYN queue bypass করে state-টা SYN-ACK-এর bit-এ encode করা, memory allocation ACK-র পরে; (২) backlog/ somaxconn tuning আর SYN_RECV retry-limit কমানো, সাথে edge- এ SYN rate-limiting। মূল নীতি: half-open state-এর জন্য attacker-কে memory-র দাম দিতে দেওয়া যাবে না।

এরপর কী

এরপর কী

Connection জন্মাল — এখন সেই promise-টা: নির্ভরযোগ্য, ক্রমবিন্যস্ত byte-stream। কিন্তু IP তো packet হারায়!

পরের লেসনে:

  • Cumulative ACK কীভাবে একটা সংখ্যা দিয়ে “এর আগের সব পেয়েছি” বলে
  • RTT মেপে EWMA-তে মসৃণ করে RTO (retransmission timeout) হিসাব — আর exponential backoff
  • Fast retransmit: কেন ঠিক ৩টা duplicate ACK — আর কেন এটা RTO-র চেয়ে সেকেন্ডে সেকেন্ড দ্রুত
  • Sliding window — flow control-এর যন্ত্র

আজকের ack = seq + 1 দেখেছেন; পরের লেসনে সেই +1-ই পুরো reliability-র প্রাণ।

আরও পড়ুন

  • RFC 9293 — Transmission Control Protocol (updated spec) — W. Eddy (ed.), IETF · TCP-র বর্তমান authoritative specification — handshake, state machine, MSL সব এখানে
  • TCP/IP Illustrated, Volume 1: The Protocols — Connection Establishment and Termination chapters — W. Richard Stevens · Handshake ও teardown-এর wire-level capture সহ ক্লাসিক আলোচনা
  • Computer Networking: A Top-Down Approach — Transport Layer — Kurose & Ross · Connection management আর state variable-গুলোর পাঠ্যপুস্তক-ধাঁচের ব্যাখ্যা