Thuta Learning
Computer Networking
ProjectsDevOps & Toolsbeginner

Project: Packet Header Parser တစ်ခု တည်ဆောက်ခြင်း

ဒီခန်းပြီးရင် ဘာတတ်သွားမလဲ

  • Project: Packet Header Parser တစ်ခု တည်ဆောက်ခြင်း concept ကို နားလည်ရှင်းပြနိုင်ရန်
  • Diagram ကို ဖတ်ပြီး packet/data ဘယ်လိုသွားလာသလဲ ခြေရာခံနိုင်ရန်
  • နမူနာ code ကို ကိုယ်တိုင် run ပြီး output စစ်နိုင်ရန်

နားလည်ထားရမယ့် အချက်

ဒီ 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 ဖြစ်နှင့်ပြီးသား ဖြစ်လို့ပါ။

text
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 လုပ်နေတယ်" ကို ခွဲခြားလို့ မရတော့ပါဘူး။

အတူတူ စမ်းရေးကြည့်မယ်

python
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)
You should see
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

ဒီနေရာမှာ လူအများမှားတတ်တယ်

  • 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 နဲ့ တိုက်ကြည့်တာ မဟုတ်ပါဘူး။
  • နမူနာ code ကို production network ပေါ် တိုက်ရိုက်မစမ်းဘဲ local/test environment တွင် အရင်အတည်ပြုပါ။

လေ့ကျင့်ခန်း

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 ကို ဒီအတိုင်းပဲ ဖမ်းမိတာပါ။

You'll know it worked when: 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

Project: Packet Header Parser တစ်ခု တည်ဆောက်ခြင်း | Thuta Learning