နားလည်ထားရမယ့် အချက်
IP layer က packet တစ်ခုချင်းစီကို ပို့ပေးရုံပဲ လုပ်ပြီး ဘာမှ အာမမခံပါဘူး — packet တစ်ခုဟာ ပျောက်သွားနိုင်တယ်၊ နှစ်ခါ ရောက်နိုင်တယ်၊ နောက်ကျနိုင်တယ်၊ အစီအစဉ် လွဲပြီး ရောက်နိုင်တယ်။ TCP ရဲ့ တာဝန်ကတော့ ဒီအပေါ်မှာ ယုံကြည်စိတ်ချရပြီး အစီအစဉ်ကျတဲ့ byte stream တစ်ခုကို တည်ဆောက်ဖို့ ဖြစ်ပြီး၊ stream က ဘယ်နေရာက စတယ်ဆိုတာ နှစ်ဖက်စလုံး သဘောတူပြီးမှသာ စလုပ်လို့ရတယ်။ handshake ရဲ့ ရည်ရွယ်ချက် အဓိကက ဒီသဘောတူညီမှုပဲ။ client က သူ့ရဲ့ initial sequence number (ISN) ပါတဲ့ SYN တစ်ခု ပို့တယ်၊ server က သူ့ ISN နဲ့အတူ client ရဲ့ ISN ကို acknowledge လုပ်တဲ့ SYN-ACK ပြန်ပို့တယ်၊ ပြီးရင် client က နောက်ဆုံး ACK တစ်ခု ပို့တယ်။ segment သုံးခု လိုတာက connection ဟာ full-duplex ဖြစ်လို့ — direction နှစ်ခုစလုံးမှာ သီးခြား stream တစ်ခုစီရှိပြီး တစ်ခုစီအတွက် စမှတ်ကို ကြေညာပြီး အတည်ပြုပေးရလို့ ဖြစ်တယ်။
ISN ဟာ သုည မဟုတ်သလို ရိုးရိုး counter လည်း မဟုတ်ဘူး — random ထုတ်ထားတာ ဖြစ်တယ်။ အကြောင်းက နှစ်ချက် ရှိတယ်။ ပထမတစ်ချက်က off-path attacker တစ်ယောက်ဟာ traffic ကို လုံးဝ မမြင်ရဘဲနဲ့တောင် နောက်လာမယ့် sequence number ကို မှန်းလို့ရရင် သူများရဲ့ connection ထဲကို data ထိုးထည့်နိုင်လို့ ဖြစ်တယ်။ ဒုတိယတစ်ချက်က four-tuple အတူတူနဲ့ ရှေ့က connection ဟောင်းတစ်ခုကနေ နောက်ကျကျန်ခဲ့တဲ့ segment တစ်ခုဟာ connection အသစ်ထဲမှာ တရားဝင် data အဖြစ် လက်ခံခံရ သွားနိုင်လို့ ဖြစ်တယ်။ ISN ကို random ထုတ်လိုက်ခြင်းက ဒီနှစ်ခုစလုံးကို လက်တွေ့မဖြစ်နိုင်အောင် လုပ်ပေးတယ်။
ဘက်တစ်ဖက်စီမှာ state machine တစ်ခုစီ ရှိတယ်။ client က SYN_SENT ကနေ ESTABLISHED ကို သွားပြီး၊ server က LISTEN မှာ စောင့်နေရာကနေ SYN_RECEIVED ပြီးမှ ESTABLISHED ရောက်တယ်။ ပိတ်တဲ့အခါကျတော့ သုံးဆင့် မဟုတ်ဘဲ လေးဆင့် ဖြစ်တယ် — direction တစ်ခုစီကို သူ့ကိုယ်ပိုင် FIN နဲ့ ACK နဲ့ သီးခြား ပိတ်ရလို့၊ ဒါကြောင့်ပဲ တစ်ဖက်က ပို့တာ ရပ်သွားပြီးတောင် ကျန်တစ်ဖက်က ဆက်ပို့နေနိုင်တာ ဖြစ်တယ်။ အရင်ဆုံး ပိတ်လိုက်တဲ့ ဘက်က maximum segment lifetime ရဲ့ နှစ်ဆ ကြာအောင် TIME_WAIT ထဲမှာ နေရတယ် — ဒါက port ကို တမင် ကိုင်ထားပြီး၊ သေသွားပြီးသား connection ကနေ နောက်ကျကျန်ခဲ့တဲ့ segment တစ်ခုကို four-tuple အတူတူ သုံးတဲ့ connection အသစ်ရဲ့ data အဖြစ် မှားမမှတ်အောင် ကာကွယ်ပေးနေတာ ဖြစ်တယ်။
TCP THREE-WAY HANDSHAKE AND FOUR-WAY CLOSE
------------------------------------------
Client Server
| |
CLOSED LISTEN
|------------------ SYN seq=x ----------------->|
SYN_SENT SYN_RECEIVED
|<---------- SYN-ACK seq=y ack=x+1 ------------|
|------------ ACK seq=x+1 ack=y+1 ------------>|
ESTABLISHED ESTABLISHED
| ... application data flows ... |
|----------- FIN (I am done sending) ---------->|
FIN_WAIT_1 CLOSE_WAIT
|<-------------------- ACK ----------------------|
|<---------- FIN (now I am done too) -----------|
LAST_ACK
|--------------------- ACK --------------------->|
TIME_WAIT CLOSED
waits 2 x MSL, then CLOSEDလက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်
mobile app တစ်ခုရဲ့ login က 3G ပေါ်မှာ ဘာလို့ ဒီလောက် နှေးနေသလဲ ဆိုတာကို debug လုပ်နေတယ် ဆိုပါစို့။ request ကိုယ်တိုင်က 400 bytes ပဲ ရှိပြီး server က 20 ms အတွင်း ပြန်ဖြေပေမယ့် user တွေက တစ်စက္ကန့်နီးပါး စောင့်နေရတယ်။ handshake ကို လိုက်ရေတွက်ကြည့်ရင် အဖြေ ပေါ်လာတယ်။ POST ရဲ့ byte တစ်ခုမှ ဖုန်းကနေ မထွက်ခင်မှာ TCP က SYN နဲ့ SYN-ACK အတွက် round trip အပြည့် တစ်ခေါက် သုံးလိုက်ပြီးသား ဖြစ်တယ်။ ပြီးရင် TLS က နောက်ထပ် တစ်ခေါက် (သို့) နှစ်ခေါက် ထပ်ယူတယ်။ round-trip time 200 ms ရှိတဲ့ link တစ်ခုပေါ်မှာ application protocol မစခင်မှာကတည်းက 400-600 ms ကုန်သွားပြီ ဖြစ်တယ်။ ဒါကြောင့်ပဲ connection reuse ဟာ အကျိုးအရှိဆုံး configuration တစ်ခု ဖြစ်တာ — connection pool ထားတဲ့ HTTP client (သို့) Connection: keep-alive ကို လေးစားတဲ့ server ဟာ handshake ကို တစ်ခါပဲ ပေးဆပ်ပြီး request ရာနဲ့ချီအတွက် မျှခံလိုက်နိုင်တယ်။ link နှေးတဲ့နေရာမှာ HTTP/2 connection တစ်ခုတည်းက HTTP/1.1 connection ခြောက်ခုထက် သာနေတာလည်း ဒီအကြောင်းကြောင့်ပဲ။
production မှာ handshake ကို နောက်ထပ် တွေ့ရတဲ့နေရာက TIME_WAIT ဖြစ်တယ်။ request တစ်ခုကို connection တစ်ခုစီ ဖွင့် ပိတ်လုပ်နေတဲ့ load generator (သို့) proxy ဟာ အရင်ပိတ်တဲ့ ဘက်မှာ TIME_WAIT ငြိနေတဲ့ socket ထောင်ချီ စုပုံလာပြီး တစ်ခုစီက local port တစ်ခုစီကို နှစ်မိနစ်ကြာ ကိုင်ထားတယ်။ ephemeral port ကုန်သွားရင် 'cannot assign requested address' စတင် တွေ့ရတယ်။ ဖြေရှင်းနည်းက TIME_WAIT ကို လျှော့ဖို့ မဟုတ်ဘဲ၊ ချက်ချင်း ပြန်ဖွင့်တော့မယ့် connection ကို ပိတ်နေတာကို ရပ်ဖို့ ဖြစ်တယ်။
အတူတူ စမ်းရေးကြည့်မယ်
import struct
# TCP flag bits, least significant bit first (bit 0 = FIN)
FLAG_NAMES = ["FIN", "SYN", "RST", "PSH", "ACK", "URG"]
def build_tcp_header(src_port, dst_port, seq, ack, flags, window):
"""Pack a 20-byte TCP header. data_offset=5 means 5 * 4 = 20 bytes."""
data_offset = 5
offset_flags = (data_offset << 12) | flags
return struct.pack(
"!HHIIHHHH",
src_port, dst_port, seq, ack, offset_flags, window, 0, 0
)
def describe(label, header):
src, dst, seq, ack, off_flags, window, _csum, _urg = struct.unpack(
"!HHIIHHHH", header
)
header_len = (off_flags >> 12) * 4
bits = off_flags & 0x3F
names = [FLAG_NAMES[i] for i in range(6) if bits & (1 << i)]
print(label)
print(" bytes on wire : %d (offset field says %d)" %
(len(header), header_len))
print(" ports :", src, "->", dst)
print(" seq / ack :", seq, "/", ack)
print(" window :", window)
print(" flags :", "+".join(names), "(0x%02x)" % bits)
# Client picks a random initial sequence number. Hard-coded here so the
# example is reproducible; a real stack would generate it unpredictably.
CLIENT_ISN = 1105843201
SERVER_ISN = 3221225472
syn = build_tcp_header(49152, 443, CLIENT_ISN, 0, 0x02, 64240)
syn_ack = build_tcp_header(443, 49152, SERVER_ISN, CLIENT_ISN + 1, 0x12, 65535)
ack = build_tcp_header(49152, 443, CLIENT_ISN + 1, SERVER_ISN + 1, 0x10, 64240)
describe("1. SYN (client -> server)", syn)
describe("2. SYN-ACK (server -> client)", syn_ack)
describe("3. ACK (client -> server)", ack)
print()
print("raw SYN hex :", syn.hex())
print("handshake costs 0 bytes of application data, 3 segments")1. SYN (client -> server)
bytes on wire : 20 (offset field says 20)
ports : 49152 -> 443
seq / ack : 1105843201 / 0
window : 64240
flags : SYN (0x02)
2. SYN-ACK (server -> client)
bytes on wire : 20 (offset field says 20)
ports : 443 -> 49152
seq / ack : 3221225472 / 1105843202
window : 65535
flags : SYN+ACK (0x12)
3. ACK (client -> server)
bytes on wire : 20 (offset field says 20)
ports : 49152 -> 443
seq / ack : 1105843202 / 3221225473
window : 64240
flags : ACK (0x10)
raw SYN hex : c00001bb41e9d401000000005002faf000000000
handshake costs 0 bytes of application data, 3 segments၅ မိနစ် စမ်းကြည့်
code ထဲက build_tcp_header ကို ပြင်ပြီး FIN+ACK (0x11) နဲ့ RST (0x04) segment နှစ်ခု ဆောက်ကြည့်ပါ။ ပြီးရင် data 100 bytes ပါတဲ့ segment တစ်ခုကို seq=x နဲ့ ပို့တယ် ဆိုရင် တစ်ဖက်က ပြန်ပို့မယ့် ACK number ဘယ်လောက် ဖြစ်သင့်သလဲ၊ SYN တစ်ခုက data byte ဘယ်နှစ်ခု ယူသလဲ ဆိုတာ တွက်ကြည့်ပါ။
သတိလေးတစ်ချက်
handshake ကို 'packet သုံးခု = round trip သုံးခေါက်' လို့ ဖတ်မိတာ။ တကယ်က round trip တစ်ခုခွဲပဲ ဖြစ်ပြီး data မပို့ခင် ပေးဆပ်ရတာက RTT တစ်ခေါက်သာ ဖြစ်တယ်။
ephemeral port ကုန်တာကို TIME_WAIT လျှော့ပြီး (ဒါမှမဟုတ် Linux အသစ်တွေမှာ ဖယ်ရှားပြီးသား tcp_tw_recycle နဲ့) ဖြေရှင်းဖို့ ကြိုးစားတာ။ တကယ့် bug က request တိုင်းအတွက် connection အသစ် ဖွင့်နေတဲ့ client ဘက်မှာ ရှိတယ်။
RFC 9293 - Transmission Control Protocol (TCP) — Computer Networking