"""
CS 60 - Day 9 exercise: parse the Layer 2 and 3 header of a real frame.

Author: Tim Pierson, Dartmouth CS60, Fall 2026
        Developed with Claude Code Opus 5.5 Medium

Run:
    Terminal 1:  sudo python3 parse_headers_starter.py            # defaults to ens160
                 sudo python3 parse_headers_starter.py eth0       # or name an interface
    Terminal 2:  ping -c 3 8.8.8.8

On Day 7 we used SOCK_DGRAM, which hid the 14-byte Ethernet header so Layer 3
started at byte 0. Today we use SOCK_RAW and those 14 bytes come back:

    | dst MAC | src MAC | type |      rest of the packet ...
      6 bytes   6 bytes   2        (the Layer 3 stuff from Day 7)
      0-5       6-11      12-13

Your job: oomplete parse_layer2 and parse_layer3

Needs sudo (raw sockets). Run on the VM's real interface, not a VPN -- a
tunnel has no MACs.
"""

import socket
import sys

ETH_P_ALL = 0x0003


def get_nibble_left(byte):
    """
    Extracts the left (most significant / high-order) 4 bits of a byte.
    Used for extracting fields like the IP Version.
    """
    # Shift the high 4 bits to the rightmost position (e.g., 0x45 -> 0x04)
    return byte >> 4


def get_nibble_right(byte):
    """
    Extracts the right (least significant / low-order) 4 bits of a byte.
    Used for extracting fields like the IP Internet Header Length (IHL).
    """
    # Isolate the low 4 bits using a bitwise AND mask of 1111 (0x0F)
    return byte & 0x0F
  
def parse_layer3(bytes):
    """
    Parse Layer 3 bytes from NIC interface
    Skip non-IPv4 packets
    Return a string representation of the Layer 3 header
    """
    #skip layer 2 header (first 14 bytes)
    ip_header = bytes[14:]

    #TODO: get version, ip_header_length, protocol, ttl, src_ip, dst_ip

    
    header = f"\tIP Version: {version}\n"
    header += f"\tIHL (Header Length): {ip_header_length} bytes\n"
    header += f"\tProtocol: {protocol}\n"
    header += f"\tTTL: {ttl}\n"
    header += f"\tSource IP: {src_ip}\n"
    header += f"\tDestination IP: {dest_ip}\n"
    return header

def parse_layer2(bytes):
    """
    Parse Layer 2 bytes from NIC interface
    Return a string representation of the Layer 2 header
    """
    # EtherType -- bytes 12 and 13, as one number
    # 0x0800 = IPv4, 0x0806 = ARP, 0x86dd = IPv6
    # byte 12 is the high byte, byte 13 the low byte.

    #TODO: get ether_type, dst_mac, src_mac
   

    header = f"dst MAC: {dst_mac}\n"
    header += f"src MAC: {src_mac}\n"
    header += f"type: 0x{ether_type:04x} ({TYPES.get(ether_type, 'other')})"
    return header



# ---------------------------------------------------------------------------
# Provided: EtherType names for the printout
# ---------------------------------------------------------------------------
TYPES = {0x0800: "IPv4", 0x0806: "ARP", 0x86dd: "IPv6"}


if __name__ == "__main__":
    # Interface from the command line, or default to the VM's interface.
    interface = sys.argv[1] if len(sys.argv) > 1 else "ens160"

    try:
        sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
        sock.bind((interface, 0))
    except PermissionError:
        sys.exit("Raw sockets need root. Try: sudo python3 parse_ethernet_starter.py")
    except OSError as e:
        sys.exit(f"Could not open {interface!r}: {e}")

    print(f"Listening on {interface}. In another terminal run:  ping -c 3 8.8.8.8")
    print("(Ctrl+C to stop)\n")

    try:
        while True:
            raw_bytes, _ = sock.recvfrom(65535)
            if len(raw_bytes) < 14:
                continue # not an ethernet header

            #print Layer 2 and 3 header fields
            print("-"*40)
            print("### Layer 2 ###")
            print(parse_layer2(raw_bytes))
            print("\t### Layer 3 ###")
            print(parse_layer3(raw_bytes))
            print("-"*40)
            
    except KeyboardInterrupt:
        print("\nStopped.")
