နားလည်ထားရမယ့် အချက်
ဒီ parser ဟာ encapsulation ဆိုတာ ပုံကြမ်းတစ်ခု မဟုတ်တော့ဘဲ byte offset တစ်ခု ဖြစ်လာတဲ့ နေရာပါ။ Script က byte 40 လုံးကို တည်ဆောက်ပြီး၊ NIC driver တစ်ခု၊ tcpdump ဒါမှမဟုတ် firewall တစ်ခု လုပ်သလိုမျိုး ပြန်ဖြုတ်ပြပါတယ် — byte တွေရဲ့ "အဓိပ္ပာယ်" ကို RFC မှာ သတ်မှတ်ထားတဲ့ layout ကလွဲလို့ ဘာမှ မသိဘဲနဲ့ပါ။
အရင်က သင်ခဲ့တဲ့ concept လေးခုက အလုပ်လုပ်ပေးပါတယ်။ Encapsulation က ပုံသဏ္ဌာန်ကို ပေးတယ် — TCP header ဟာ packet ထဲ တစ်နေရာရာမှာ ရှိနေတာ မဟုတ်ဘဲ IPv4 header ရဲ့ တိုက်ရိုက် နောက်မှာ ရှိပြီး၊ သူ့ကို byte 0 ထဲက IHL nibble ကို ဖတ်ပြီး 4 နဲ့ မြှောက်လိုက်ရင် ရှာတွေ့ပါတယ်။ ဒါကြောင့်ပဲ code မှာ packet[20:] မဟုတ်ဘဲ packet[ip_len:] လို့ ရေးထားတာပါ — 20 ဟာ IP options မပါမှသာ မှန်ပြီး၊ အဲဒါကို hardcode လုပ်ထားတာက options ပါလာတဲ့ ပထမဆုံး packet မှာ parser ပျက်သွားစေတဲ့ အကြောင်းရင်းပါ။ Header layout တွေက format string တွေကို ပေးတယ် — "!BBHHHBBH4s4s" ဟာ IPv4 header ကို ဘယ်ကနေ ညာ ဆွဲထားတဲ့ ပုံအတိုင်းပါပဲ၊ "!HHIIHHHH" က TCP အတွက်ပါ။ "!" က network byte order (big-endian) ကို ဆိုလိုပြီး၊ ဒါကပဲ manual shifting မဟုတ်ဘဲ struct ကို သုံးသင့်တဲ့ အကြောင်းရင်း တစ်ခုလုံးပါ။
Bit masking က flag တွေကို ပေးတယ်။ TCP ဟာ 4-bit data offset နဲ့ flag bit ကိုးလုံးကို 16-bit word တစ်လုံးထဲ ထည့်သွင်းထားလို့၊ offset_flags >> 12 က header length ကို ပြန်ရပြီး offset_flags & 0x01FF က flag တွေကို ပြန်ရပါတယ်၊ ပြီးရင် flag တစ်ခုချင်းစီက သူ့ bit နဲ့ & တစ်ချက် လုပ်ရုံပါပဲ။ နောက်ဆုံးမှာ 4-tuple ဟာ အလိုအလျောက် ထွက်လာပါတယ် — source IP နဲ့ destination IP က layer 3 ကနေ၊ source port နဲ့ destination port က layer 4 ကနေ၊ protocol နဲ့ ပေါင်းလိုက်ရင် လမ်းကြောင်းတစ်လျှောက်က NAT table နဲ့ connection tracker တိုင်းက key လုပ်တဲ့ အရာ ဖြစ်သွားပါတယ်။ Checksum စစ်တာက ဒါကို တကယ့်အရာ ဖြစ်စေတဲ့ အသေးစိတ်ချက်ပါ — မှန်ကန်တဲ့ header တစ်ခုပေါ်မှာ Internet checksum ကို ပြန်တွက်ရင် သုည ထွက်ပါတယ်၊ ဘာလို့လဲဆိုတော့ သိမ်းထားတဲ့ တန်ဖိုးက ကျန်တဲ့ word အားလုံးရဲ့ one's complement ဖြစ်နှင့်ပြီးသား ဖြစ်လို့ပါ။
PACKET PARSER: BYTES IN, FIELDS OUT
-----------------------------------
RAW BYTES (40 total)
+----------------------------------------------------------+
| 4500 0028 1c46 4000 4006 26fd c0a8 010a 5db8 d822 ... |
+----------------------------------------------------------+
|
| byte 0 low nibble = IHL = 5 words = 20 bytes
v
+----------------------------+----------------------------+
| IPv4 HEADER [0:20] | TCP HEADER [20:40] |
+----------------------------+----------------------------+
| |
| struct.unpack | struct.unpack
| "!BBHHHBBH4s4s" | "!HHIIHHHH"
v v
+----------------------------+ +----------------------------+
| version, IHL, total_length | | sport, dport, seq, ack |
| ttl, protocol, checksum | | offset_flags, window |
| src IP, dst IP | +----------------------------+
+----------------------------+ |
| | & 0x001 .. 0x100
v v
+----------------------------+ +------------------------+
| checksum16(header) == 0 ? | | FIN SYN RST PSH ACK .. |
+----------------------------+ +------------------------+
| |
+----------------+-------------------+
v
THE 4-TUPLE: src IP : sport -> dst IP : dport / TCPလက်တွေ့ scenario နဲ့ ချိတ်ကြည့်မယ်
ဒီ tool နဲ့ tcpdump ကို အစားထိုးမှာ မဟုတ်ပါဘူး။ ဒီလို parser တစ်ခုကို ရေးဖြစ်တာက summary line တစ်ကြောင်း မလုံလောက်တဲ့ အချိန်မှာပါ — capture တစ်ခုကို ကြည့်နေရင်း၊ ဘယ် tool ကမှ တိတိကျကျ မဖြေပေးတဲ့ မေးခွန်းကို ဖြေဖို့ လိုတဲ့ အခါပါ — ဒါတွေ retransmit တွေလား ဒါမှမဟုတ် sequence number က တကယ် နောက်ပြန်သွားနေတာလား၊ MTU သေးတဲ့ tunnel တစ်ခုက ချထားတဲ့ packet တွေမှာ DF bit set ဖြစ်နေလား၊ တစ်ဖက်က SYN ပို့နေတာလား SYN-ACK ပို့နေတာလား ဆိုတာမျိုးပါ။ ဒီ project က မွေးပေးတဲ့ အလေ့အထက packet တစ်ခုကို "byte တွေ + layout တစ်ခု" အဖြစ် ဖတ်တတ်ဖို့ပါ၊ ဒါဟာ နောင်တွေ့မယ့် binary protocol မှန်သမျှအတွက် လိုအပ်တဲ့ အလေ့အထ အတူတူပါပဲ။
လက်တွေ့ အကျိုးသက်ရောက်မှုက နေရာနှစ်ခုမှာ ပေါ်လာပါတယ်။ ပထမတစ်ခုက MTU နဲ့ fragmentation debugging ပါ — handshake ပြီးသွားပြီးမှ ပထမဆုံး data ကြီးကြီး ပို့တဲ့ အချိန်မှာ ရပ်သွားတဲ့ connection ဟာ "DF set ဖြစ်ပြီး packet ကြီးလွန်းလို့ ICMP ကလည်း filter ခံနေရ" ဆိုတဲ့ classic ဇာတ်လမ်းပါ၊ ဒါကို packet အစစ်တွေရဲ့ total_length နဲ့ DF bit ကို ဖတ်ပြီး ရောဂါရှာရပါတယ်။ ဒုတိယက 4-tuple နဲ့ ဆက်စပ်တဲ့ အရာအားလုံး — NAT, load balancer, connection table တွေပါ။ Flow နှစ်ခု NAT table ထဲ တိုက်မိတဲ့ အခါ ဒါမှမဟုတ် firewall log က အဓိပ္ပာယ် မရှိတဲ့ အခါ၊ အဖြေက အမြဲတမ်း အဲဒီ တန်ဖိုး လေးခုကို တန်းစီ ချိန်ကြည့်ရာကနေပဲ ရပါတယ်။
လွယ်လွယ်နည်းက offset အသေတွေမှာ ဖြတ်ပြီး checksum ကို ကျော်လိုက်တာပါ။ ဒါဟာ စမ်းသပ်ခဲ့တဲ့ packet တွေမှာ အလုပ်လုပ်ပြီး မစမ်းခဲ့တဲ့ packet တွေမှာ ပျက်ပါတယ် — options တွေက TCP field တိုင်းကို ညာဘက်ကို 4 ဒါမှမဟုတ် 8 byte တွန်းပို့လိုက်ပြီး၊ checksum မစစ်ဘူးဆိုရင် "တစ်ဖက်က ထူးဆန်းတာ လုပ်နေတယ်" နဲ့ "ငါ အမှိုက်တွေကို parse လုပ်နေတယ်" ကို ခွဲခြားလို့ မရတော့ပါဘူး။
အတူတူ စမ်းရေးကြည့်မယ်
import struct
import ipaddress
import binascii
PROTOCOLS = {1: "ICMP", 6: "TCP", 17: "UDP"}
# TCP flag bits, low bit first, as they sit in the 9-bit flags field.
TCP_FLAGS = [
(0x001, "FIN"), (0x002, "SYN"), (0x004, "RST"), (0x008, "PSH"),
(0x010, "ACK"), (0x020, "URG"), (0x040, "ECE"), (0x080, "CWR"),
(0x100, "NS"),
]
def checksum16(data):
"""The standard Internet checksum: one's complement of the one's
complement sum of all 16-bit words."""
if len(data) % 2:
data += b"\x00"
total = 0
for (word,) in struct.iter_unpack("!H", data):
total += word
while total >> 16:
total = (total & 0xFFFF) + (total >> 16)
return ~total & 0xFFFF
def build_ipv4_header(src, dst, payload_len, ttl=64, proto=6, ident=0x1C46):
version_ihl = (4 << 4) | 5 # version 4, IHL 5 words = 20 bytes
total_len = 20 + payload_len
flags_frag = 0x4000 # Don't Fragment, offset 0
header = struct.pack(
"!BBHHHBBH4s4s",
version_ihl, 0, total_len, ident, flags_frag, ttl, proto, 0,
ipaddress.IPv4Address(src).packed,
ipaddress.IPv4Address(dst).packed,
)
csum = checksum16(header) # computed with the checksum field zeroed
return header[:10] + struct.pack("!H", csum) + header[12:]
def build_tcp_header(sport, dport, seq, ack, flags, window=64240):
offset_reserved = (5 << 12) | flags # data offset 5 words = 20 bytes
return struct.pack("!HHIIHHHH", sport, dport, seq, ack,
offset_reserved, window, 0, 0)
def parse_ipv4(data):
(version_ihl, tos, total_len, ident, flags_frag, ttl, proto,
csum, src, dst) = struct.unpack("!BBHHHBBH4s4s", data[:20])
return {
"version": version_ihl >> 4,
"ihl_words": version_ihl & 0x0F,
"tos": tos,
"total_length": total_len,
"id": ident,
"df": bool(flags_frag & 0x4000),
"mf": bool(flags_frag & 0x2000),
"ttl": ttl,
"protocol": proto,
"checksum": csum,
"src": str(ipaddress.IPv4Address(src)),
"dst": str(ipaddress.IPv4Address(dst)),
}
def parse_tcp(data):
(sport, dport, seq, ack, offset_flags, window, csum, urg) = \
struct.unpack("!HHIIHHHH", data[:20])
return {
"sport": sport,
"dport": dport,
"seq": seq,
"ack": ack,
"offset_words": offset_flags >> 12,
"flags": offset_flags & 0x01FF,
"window": window,
"checksum": csum,
"urgent": urg,
}
def flag_names(flags):
return [name for bit, name in TCP_FLAGS if flags & bit]
def report(packet):
ip = parse_ipv4(packet)
ip_len = ip["ihl_words"] * 4
tcp = parse_tcp(packet[ip_len:])
print("PACKET HEADER REPORT")
print("=" * 64)
print("Raw bytes (%d total):" % len(packet))
hexs = binascii.hexlify(packet).decode()
for i in range(0, len(hexs), 32):
print(" %04x %s" % (i // 2, " ".join(
hexs[j:j + 4] for j in range(i, min(i + 32, len(hexs)), 4))))
print()
print("-- LAYER 3: IPv4 " + "-" * 47)
print(" Version : %d" % ip["version"])
print(" IHL : %d words (%d bytes)" % (ip["ihl_words"], ip_len))
print(" Total length : %d bytes (payload %d)" % (ip["total_length"],
ip["total_length"] - ip_len))
print(" Identification : 0x%04x" % ip["id"])
print(" Flags : DF=%d MF=%d" % (ip["df"], ip["mf"]))
print(" TTL : %d" % ip["ttl"])
print(" Protocol : %d (%s)" % (ip["protocol"],
PROTOCOLS.get(ip["protocol"], "unknown")))
print(" Header checksum : 0x%04x" % ip["checksum"])
# A correct header re-checksums to zero, because the stored value is
# already the complement of the sum of every other word.
verify = checksum16(packet[:ip_len])
print(" Checksum verify : %s (recomputed 0x%04x)" %
("VALID" if verify == 0 else "INVALID", verify))
print(" Source IP : %s" % ip["src"])
print(" Destination IP : %s" % ip["dst"])
print()
print("-- LAYER 4: TCP " + "-" * 48)
print(" Source port : %d" % tcp["sport"])
print(" Destination port : %d" % tcp["dport"])
print(" Sequence number : %d (0x%08x)" % (tcp["seq"], tcp["seq"]))
print(" Ack number : %d" % tcp["ack"])
print(" Data offset : %d words (%d bytes)" % (tcp["offset_words"],
tcp["offset_words"] * 4))
print(" Flags : 0x%03x -> %s" % (tcp["flags"],
" ".join(flag_names(tcp["flags"])) or "none"))
for bit, name in TCP_FLAGS:
print(" %-4s %d" % (name, 1 if tcp["flags"] & bit else 0))
print(" Window : %d" % tcp["window"])
print()
print("-- THE 4-TUPLE " + "-" * 49)
print(" %s:%d -> %s:%d" % (ip["src"], tcp["sport"], ip["dst"], tcp["dport"]))
print(" protocol %s" % PROTOCOLS.get(ip["protocol"], "unknown"))
tcp_header = build_tcp_header(50912, 443, 0x9ECF1D2A, 0, flags=0x002) # SYN
ip_header = build_ipv4_header("192.168.1.10", "93.184.216.34", len(tcp_header))
report(ip_header + tcp_header)
PACKET HEADER REPORT
================================================================
Raw bytes (40 total):
0000 4500 0028 1c46 4000 4006 26fd c0a8 010a
0010 5db8 d822 c6e0 01bb 9ecf 1d2a 0000 0000
0020 5002 faf0 0000 0000
-- LAYER 3: IPv4 -----------------------------------------------
Version : 4
IHL : 5 words (20 bytes)
Total length : 40 bytes (payload 20)
Identification : 0x1c46
Flags : DF=1 MF=0
TTL : 64
Protocol : 6 (TCP)
Header checksum : 0x26fd
Checksum verify : VALID (recomputed 0x0000)
Source IP : 192.168.1.10
Destination IP : 93.184.216.34
-- LAYER 4: TCP ------------------------------------------------
Source port : 50912
Destination port : 443
Sequence number : 2664373546 (0x9ecf1d2a)
Ack number : 0
Data offset : 5 words (20 bytes)
Flags : 0x002 -> SYN
FIN 0
SYN 1
RST 0
PSH 0
ACK 0
URG 0
ECE 0
CWR 0
NS 0
Window : 64240
-- THE 4-TUPLE -------------------------------------------------
192.168.1.10:50912 -> 93.184.216.34:443
protocol TCP
၅ မိနစ် စမ်းကြည့်
build_tcp_header() ကို flags=0x012 (SYN + ACK) နဲ့ ack=0x9ECF1D2B ပေးပြီး၊ TTL ကို 64 အစား 55 ပြောင်းကာ ပြန် run ကြည့်ပါ။ Report မှာ SYN နဲ့ ACK နှစ်ခုလုံး 1 ပြရမယ်၊ checksum ကလည်း VALID ဖြစ်နေရမယ် (build_ipv4_header က ပြန်တွက်ပေးလို့ပါ)။ ပြီးရင် ပိုစိတ်ဝင်စားစရာ ကောင်းတဲ့ အပိုင်း — packet bytes ထဲက TTL byte (index 8) ကို လက်နဲ့ တစ်ခု ပြောင်းလိုက်ပြီး checksum ကို မပြင်ဘဲ report ကို ပြန် run ပါ။ "Checksum verify" က INVALID ဖြစ်သွားပြီး ပြန်တွက်လို့ရတဲ့ တန်ဖိုးက သုည မဟုတ်တော့တာကို တွေ့ရပါလိမ့်မယ် — router တစ်ခုက corrupt ဖြစ်နေတဲ့ packet ကို ဒီအတိုင်းပဲ ဖမ်းမိတာပါ။
သတိလေးတစ်ချက်
IPv4 header ကို 20 byte လို့ အမြဲ ယူဆပြီး IHL ကို လျစ်လျူရှုတာ။ Packet အများစုမှာ options မပါလို့ အလုပ်ဖြစ်နေမယ်၊ ဒါပေမယ့် record-route ဒါမှမဟုတ် timestamp option ပါလာတဲ့ packet တစ်ခု ရောက်လာတာနဲ့ TCP field တိုင်း 4 ဒါမှမဟုတ် 8 byte ရွှေ့သွားပြီး၊ port တွေက ဆန်းပြားတဲ့ ဂဏန်းတွေ ဖြစ်သွားမယ်၊ flag တွေက အဓိပ္ပာယ် မရှိတော့ပါဘူး။ Header အလျားကို byte 0 ကနေပဲ အမြဲ ဖတ်ပါ။
Checksum စစ်ပုံ မှားတာ။ Header အသစ် တည်ဆောက်တဲ့ အခါ checksum field ကို သုည ထားပြီးမှ တွက်ရပါတယ် — မဟုတ်ရင် ဟောင်းနေတဲ့ တန်ဖိုးက ပေါင်းလဒ်ထဲ ရောပါသွားပါလိမ့်မယ်။ လက်ခံရရှိတဲ့ header ကို စစ်တဲ့ အခါကျတော့ ပြောင်းပြန်ပါ — field ကို သုည မထားပါနဲ့၊ header အပြည့်အစုံပေါ်မှာ ပြန်တွက်ပြီး ရလဒ် သုည ဖြစ်မဖြစ် ကြည့်ပါ။ ရလဒ်ကို သိမ်းထားတဲ့ checksum နဲ့ တိုက်ကြည့်တာ မဟုတ်ပါဘူး။
RFC 791 -- Internet Protocol (IPv4 header format and checksum) — Computer Networking