예제 #1
0
파일: ps.py 프로젝트: ccie18643/PyTCP
 def __init__(self,
              raw_option: Optional[bytes] = None,
              opt_req_ip_addr: Optional[Ip4Address] = None) -> None:
     if raw_option:
         self.opt_code = raw_option[0]
         self.opt_len = raw_option[1]
         self.opt_req_ip_addr = Ip4Address(raw_option[2:6])
     else:
         assert opt_req_ip_addr is not None
         self.opt_code = DHCP4_OPT_REQ_IP4_ADDR
         self.opt_len = DHCP4_OPT_REQ_IP4_ADDR_LEN
         self.opt_req_ip_addr = Ip4Address(opt_req_ip_addr)
예제 #2
0
파일: ps.py 프로젝트: ccie18643/PyTCP
 def __init__(self,
              raw_option: Optional[bytes] = None,
              opt_dns: Optional[list[Ip4Address]] = None) -> None:
     if raw_option:
         self.opt_code = raw_option[0]
         self.opt_len = raw_option[1]
         self.opt_dns = [
             Ip4Address(raw_option[_:_ + 4])
             for _ in range(2, 2 + self.opt_len, 4)
         ]
     else:
         assert opt_dns is not None
         self.opt_code = DHCP4_OPT_DNS
         self.opt_len = len(opt_dns) * 4
         self.opt_dns = [Ip4Address(_) for _ in opt_dns]
예제 #3
0
파일: socket.py 프로젝트: ccie18643/PyTCP
    def _set_ip_addresses(
        self, remote_address: tuple[str, int], local_ip_address: IpAddress,
        local_port: int, remote_port: int
    ) -> tuple[Union[Ip6Address, Ip4Address], Union[Ip6Address, Ip4Address]]:
        """Validate remote address and pick appropriate local ip address as needed"""

        try:
            remote_ip_address: Union[Ip6Address, Ip4Address] = Ip6Address(
                remote_address[0]) if self._family is AF_INET6 else Ip4Address(
                    remote_address[0])
        except (Ip6AddressFormatError, Ip4AddressFormatError):
            raise gaierror(
                "[Errno -2] Name or service not known - [Malformed remote IP address]"
            )

        # This contraption here is to mimic behavior of BSD socket implementation
        if remote_ip_address.is_unspecified:
            if self._type is SOCK_STREAM:
                raise ConnectionRefusedError(
                    "[Errno 111] Connection refused - [Unspecified remote IP address]"
                )
            if self._type is SOCK_DGRAM:
                self._unreachable = True

        if local_ip_address.is_unspecified:
            local_ip_address = pick_local_ip_address(remote_ip_address)
            if local_ip_address.is_unspecified and not (local_port == 68
                                                        and remote_port == 67):
                raise gaierror(
                    "[Errno -2] Name or service not known - [Malformed remote IP address]"
                )

        assert isinstance(local_ip_address, Ip4Address) or isinstance(
            local_ip_address, Ip6Address)
        return local_ip_address, remote_ip_address
예제 #4
0
파일: socket.py 프로젝트: ccie18643/PyTCP
    def bind(self, address: tuple[str, int]) -> None:
        """Bind the socket to local address"""

        # 'bind' call will bind socket to specific / unspecified local ip address and specific local port
        # in case provided port equals zero port value will be picked automatically

        # Check if "bound" already
        if self._local_port in range(1, 65536):
            raise OSError("[Errno 22] Invalid argument - [Socket bound to specific port already]")

        local_ip_address: IpAddress

        if self._family is AF_INET6:
            try:
                if (local_ip_address := Ip6Address(address[0])) not in set(stack.packet_handler.ip6_unicast) | {Ip6Address(0)}:
                    raise OSError("[Errno 99] Cannot assign requested address - [Local IP address not owned by stack]")
            except Ip6AddressFormatError:
                raise gaierror("[Errno -2] Name or service not known - [Malformed local IP address]")

        if self._family is AF_INET4:
            try:
                if (local_ip_address := Ip4Address(address[0])) not in set(stack.packet_handler.ip4_unicast) | {Ip4Address(0)}:
                    raise OSError("[Errno 99] Cannot assign requested address - [Local IP address not owned by stack]")
            except Ip4AddressFormatError:
                raise gaierror("[Errno -2] Name or service not known - [Malformed local IP address]")

        # Sanity check on local port number
        if address[1] not in range(0, 65536):
            raise OverflowError("bind(): port must be 0-65535. - [Port out of range]")

        # Confirm or pick local port number
        if (local_port := address[1]) > 0:
            if self._is_address_in_use(local_ip_address, local_port):
                raise OSError("[Errno 98] Address already in use - [Local address already in use]")
예제 #5
0
def str_to_ip(ip_address: str) -> Optional[Union[Ip6Address, Ip4Address]]:
    """Convert string to appropriate IP address"""

    try:
        return Ip6Address(ip_address)
    except Ip6AddressFormatError:
        try:
            return Ip4Address(ip_address)
        except Ip4AddressFormatError:
            return None
예제 #6
0
def ip_version(ip_address: str) -> Optional[int]:
    """Return version of IP address string"""

    try:
        return Ip6Address(ip_address).version
    except Ip6AddressFormatError:
        try:
            return Ip4Address(ip_address).version
        except Ip4AddressFormatError:
            return None
예제 #7
0
파일: ps.py 프로젝트: ccie18643/PyTCP
 def __init__(self,
              raw_option: Optional[bytes] = None,
              opt_srv_id: Optional[Ip4Address] = None) -> None:
     if raw_option:
         self.opt_code = raw_option[0]
         self.opt_len = raw_option[1]
         self.opt_srv_id = Ip4Address(raw_option[2:6])
     else:
         assert opt_srv_id is not None
         self.opt_code = DHCP4_OPT_SRV_ID
         self.opt_len = DHCP4_OPT_SRV_ID_LEN
         self.opt_srv_id = opt_srv_id
예제 #8
0
def pick_local_ip4_address(remote_ip4_address: Ip4Address) -> Ip4Address:
    """Pick appropriate source IPv4 address based on provided destination IPv4 address"""

    # If destination belongs to any of local networks, if so pick source address from that network
    for ip4_host in stack.packet_handler.ip4_host:
        if remote_ip4_address in ip4_host.network:
            return ip4_host.address
    # If destination is external pick source from first network that has default gateway set
    for ip4_host in stack.packet_handler.ip4_host:
        if ip4_host.gateway:
            return ip4_host.address
    # In case everything else fails return unspecified
    return Ip4Address(0)
예제 #9
0
    def _send_arp_probe(self, ip4_unicast: Ip4Address) -> None:
        """Send out ARP probe to detect possible IP conflict"""

        self._phtx_arp(
            ether_src=self.mac_unicast,
            ether_dst=MacAddress(0xFFFFFFFFFFFF),
            arp_oper=ARP_OP_REQUEST,
            arp_sha=self.mac_unicast,
            arp_spa=Ip4Address(0),
            arp_tha=MacAddress(0),
            arp_tpa=ip4_unicast,
        )
        if __debug__:
            log("stack", f"Sent out ARP probe for {ip4_unicast}")
예제 #10
0
파일: socket.py 프로젝트: ccie18643/PyTCP
    def __init__(self, family: AddressFamily) -> None:
        """Class constructor"""

        super().__init__()

        self._family: AddressFamily = family
        self._type: SocketType = SOCK_DGRAM
        self._local_port: int = 0
        self._remote_port: int = 0
        self._packet_rx_md: list[UdpMetadata] = []
        self._packet_rx_md_ready: Semaphore = threading.Semaphore(0)
        self._unreachable: bool = False
        self._local_ip_address: IpAddress
        self._remote_ip_address: IpAddress

        if self._family is AF_INET6:
            self._local_ip_address = Ip6Address(0)
            self._remote_ip_address = Ip6Address(0)
        if self._family is AF_INET4:
            self._local_ip_address = Ip4Address(0)
            self._remote_ip_address = Ip4Address(0)

        if __debug__:
            log("socket", f"<g>[{self}]</> - Created socket")
예제 #11
0
    def _parse_stack_ip4_host_candidate(
        self, configured_ip4_address_candidate: list[tuple[str, Optional[str]]]
    ) -> list[Ip4Host]:
        """Parse IPv4 candidate host addresses configured in config.py module"""

        valid_address_candidate: list[Ip4Host] = []

        for str_host, str_gateway in configured_ip4_address_candidate:
            if __debug__:
                log("stack", f"Parsing ('{str_host}', '{str_gateway}') entry")
            try:
                host = Ip4Host(str_host)
            except Ip4HostFormatError:
                if __debug__:
                    log(
                        "stack",
                        f"<WARN>Invalid host address '{str_host}' format, skipping</>"
                    )
                continue
            if not host.address.is_private and not host.address.is_global:
                if __debug__:
                    log(
                        "stack",
                        f"<WARN>Invalid host address '{host.address}' type, skipping</>"
                    )
                continue
            if host.address == host.network.address or host.address == host.network.broadcast:
                if __debug__:
                    log(
                        "stack",
                        f"<WARN>Invalid host address '{host.address}' configured for network '{host.network}', skipping</>"
                    )
                continue
            if host.address in [_.address for _ in valid_address_candidate]:
                if __debug__:
                    log(
                        "stack",
                        f"<WARN>Duplicate host address '{host.address}' configured, skipping</>"
                    )
                continue
            if str_gateway is not None:
                try:
                    gateway: Optional[Ip4Address] = Ip4Address(str_gateway)
                    if gateway not in host.network or gateway in {
                            host.network.address, host.network.broadcast,
                            host.address
                    }:
                        if __debug__:
                            log(
                                "stack",
                                f"<WARN>Invalid gateway '{gateway}' configuren for host address '{host}', skipping</>"
                            )
                        continue
                except Ip4AddressFormatError:
                    if __debug__:
                        log(
                            "stack",
                            f"<WARN>Invalid gateway '{str_gateway}' format configured for host address '{host}' skipping</>"
                        )
                    continue
            else:
                gateway = None
            host.gateway = gateway
            valid_address_candidate.append(host)
            if __debug__:
                log("stack", f"Parsed ('{host}', '{host.gateway}') entry")

        return valid_address_candidate
예제 #12
0
파일: phtx.py 프로젝트: ccie18643/PyTCP
def _validate_src_ip4_address(self, ip4_src: Ip4Address, ip4_dst: Ip4Address,
                              tracker: Tracker) -> Optional[Ip4Address]:
    """Make sure source ip address is valid, supplemt with valid one as appropriate"""

    # Check if the the source IP address belongs to this stack or its set to all zeros (for DHCP client communication)
    if ip4_src not in {
            *self.ip4_unicast, *self.ip4_multicast, *self.ip4_broadcast,
            Ip4Address(0)
    }:
        if __debug__:
            log(
                "ip4",
                f"{tracker} - <WARN>Unable to sent out IPv4 packet, stack doesn't own IPv4 address {ip4_src}, dropping</>"
            )
        return None

    # If packet is a response to multicast then replace source address with primary address of the stack
    if ip4_src in self.ip4_multicast:
        if self.ip4_unicast:
            ip4_src = self.ip4_unicast[0]
            if __debug__:
                log(
                    "ip4",
                    f"{tracker} - Packet is response to multicast, replaced source with stack primary IPv4 address {ip4_src}"
                )
        else:
            if __debug__:
                log(
                    "ip4",
                    f"{tracker} - <WARN>Unable to sent out IPv4 packet, no stack primary unicast IPv4 address available, dropping</>"
                )
            return None

    # If packet is a response to limited broadcast then replace source address with primary address of the stack
    if ip4_src.is_limited_broadcast:
        if self.ip4_unicast:
            ip4_src = self.ip4_unicast[0]
            if __debug__:
                log(
                    "ip4",
                    f"{tracker} - Packet is response to limited broadcast, replaced source with stack primary IPv4 address {ip4_src}"
                )
        else:
            if __debug__:
                log(
                    "ip4",
                    f"{tracker} - <WARN>Unable to sent out IPv4 packet, no stack primary unicast IPv4 address available, dropping</>"
                )
            return None

    # If packet is a response to directed braodcast then replace source address with first stack address that belongs to appropriate subnet
    if ip4_src in self.ip4_broadcast:
        ip4_src_list = [
            _.address for _ in self.ip4_host if _.network.broadcast == ip4_src
        ]
        if ip4_src_list:
            ip4_src = ip4_src_list[0]
            if __debug__:
                log(
                    "ip4",
                    f"{tracker} - Packet is response to directed broadcast, replaced source with appropriate IPv4 address {ip4_src}"
                )
        else:
            if __debug__:
                log(
                    "ip4",
                    f"{tracker} - <WARN>Unable to sent out IPv4 packet, no appropriate stack unicast IPv4 address available, dropping</>"
                )
            return None

    # If source is unspecified try to find best match for given destination
    if ip4_src.is_unspecified:
        return pick_local_ip4_address(ip4_dst)

    return ip4_src
예제 #13
0
    def ip4_broadcast(self) -> list[Ip4Address]:
        """Return list of stack's IPv4 broadcast addresses"""

        ip4_broadcast = [_.network.broadcast for _ in self.ip4_host]
        ip4_broadcast.append(Ip4Address(0xFFFFFFFF))
        return ip4_broadcast
예제 #14
0
파일: fpp.py 프로젝트: ccie18643/PyTCP
    def spa(self) -> Ip4Address:
        """Read 'Sender protocol address' field"""

        if "_cache__spa" not in self.__dict__:
            self._cache__spa = Ip4Address(self._frame[14:18])
        return self._cache__spa
예제 #15
0
파일: fpp.py 프로젝트: ccie18643/PyTCP
    def tpa(self) -> Ip4Address:
        """Read 'Target protocol address' field"""

        if "_cache__tpa" not in self.__dict__:
            self._cache__tpa = Ip4Address(self._frame[24:28])
        return self._cache__tpa
예제 #16
0
파일: fpp.py 프로젝트: ccie18643/PyTCP
    def dst(self) -> Ip4Address:
        """Read 'Destination address' field"""

        if "_cache__dst" not in self.__dict__:
            self._cache__dst = Ip4Address(self._frame[16:20])
        return self._cache__dst
예제 #17
0
파일: fpp.py 프로젝트: ccie18643/PyTCP
    def src(self) -> Ip4Address:
        """Read 'Source address' field"""

        if "_cache__src" not in self.__dict__:
            self._cache__src = Ip4Address(self._frame[12:16])
        return self._cache__src
예제 #18
0
    def fetch(self) -> Union[tuple[str, Optional[str]], tuple[None, None]]:
        """IPv4 DHCP client"""

        s = socket.socket(family=socket.AF_INET4, type=socket.SOCK_DGRAM)
        s.bind(("0.0.0.0", 68))
        s.connect(("255.255.255.255", 67))

        dhcp_xid = random.randint(0, 0xFFFFFFFF)

        # Send DHCP Discover
        s.send(
            dhcp4.ps.Dhcp4Packet(
                dhcp_op=dhcp4.ps.DHCP4_OP_REQUEST,
                dhcp_xid=dhcp_xid,
                dhcp_ciaddr=Ip4Address("0.0.0.0"),
                dhcp_yiaddr=Ip4Address("0.0.0.0"),
                dhcp_siaddr=Ip4Address("0.0.0.0"),
                dhcp_giaddr=Ip4Address("0.0.0.0"),
                dhcp_chaddr=bytes(self._mac_address),
                dhcp_msg_type=dhcp4.ps.DHCP4_MSG_DISCOVER,
                dhcp_param_req_list=[
                    dhcp4.ps.DHCP4_OPT_SUBNET_MASK, dhcp4.ps.DHCP4_OPT_ROUTER
                ],
                dhcp_host_name="PyTCP",
            ).raw_packet)
        if __debug__:
            log("dhcp4", "Sent out DHCP Discover message")

        # Wait for DHCP Offer
        try:
            dhcp_packet_rx = dhcp4.ps.Dhcp4Packet(s.recv(timeout=5))
        except socket.ReceiveTimeout:
            if __debug__:
                log("dhcp4", "Didn't receive DHCP Offer message - timeout")
            s.close()
            return None, None

        if dhcp_packet_rx.dhcp_msg_type != dhcp4.ps.DHCP4_MSG_OFFER:
            if __debug__:
                log("dhcp4",
                    "Didn't receive DHCP Offer message - message type error")
            s.close()
            return None, None

        dhcp_srv_id = dhcp_packet_rx.dhcp_srv_id
        dhcp_yiaddr = dhcp_packet_rx.dhcp_yiaddr
        if __debug__:
            log(
                "dhcp4",
                f"ClientUdpDhcp: Received DHCP Offer from {dhcp_packet_rx.dhcp_srv_id}"
                +
                f"IP: {dhcp_packet_rx.dhcp_yiaddr}, Mask: {dhcp_packet_rx.dhcp_subnet_mask}, Router: {dhcp_packet_rx.dhcp_router}"
                +
                f"DNS: {dhcp_packet_rx.dhcp_dns}, Domain: {dhcp_packet_rx.dhcp_domain_name}",
            )

        # Send DHCP Request
        s.send(
            dhcp4.ps.Dhcp4Packet(
                dhcp_op=dhcp4.ps.DHCP4_OP_REQUEST,
                dhcp_xid=dhcp_xid,
                dhcp_ciaddr=Ip4Address("0.0.0.0"),
                dhcp_yiaddr=Ip4Address("0.0.0.0"),
                dhcp_siaddr=Ip4Address("0.0.0.0"),
                dhcp_giaddr=Ip4Address("0.0.0.0"),
                dhcp_chaddr=bytes(self._mac_address),
                dhcp_msg_type=dhcp4.ps.DHCP4_MSG_REQUEST,
                dhcp_srv_id=dhcp_srv_id,
                dhcp_req_ip_addr=dhcp_yiaddr,
                dhcp_param_req_list=[
                    dhcp4.ps.DHCP4_OPT_SUBNET_MASK, dhcp4.ps.DHCP4_OPT_ROUTER
                ],
                dhcp_host_name="PyTCP",
            ).raw_packet)

        if __debug__:
            log(
                "dhcp4",
                f"Sent out DHCP Request message to {dhcp_packet_rx.dhcp_srv_id}"
            )

        # Wait for DHCP Ack
        try:
            dhcp_packet_rx = dhcp4.ps.Dhcp4Packet(s.recv(timeout=5))
        except socket.ReceiveTimeout:
            if __debug__:
                log("dhcp4", "Didn't receive DHCP ACK message - timeout")
            s.close()
            return None, None

        if dhcp_packet_rx.dhcp_msg_type != dhcp4.ps.DHCP4_MSG_ACK:
            if __debug__:
                log("dhcp4",
                    "Didn't receive DHCP ACK message - message type error")
            s.close()
            return None, None

        if __debug__:
            log(
                "dhcp4",
                f"Received DHCP Offer from {dhcp_packet_rx.dhcp_srv_id}" +
                f"IP: {dhcp_packet_rx.dhcp_yiaddr}, Mask: {dhcp_packet_rx.dhcp_subnet_mask}, Router: {dhcp_packet_rx.dhcp_router}"
                +
                f"DNS: {dhcp_packet_rx.dhcp_dns}, Domain: {dhcp_packet_rx.dhcp_domain_name}",
            )
        s.close()

        assert dhcp_packet_rx.dhcp_subnet_mask is not None
        return (
            str(dhcp_packet_rx.dhcp_yiaddr) +
            str(dhcp_packet_rx.dhcp_subnet_mask),
            str(dhcp_packet_rx.dhcp_router[0])
            if dhcp_packet_rx.dhcp_router is not None else None,
        )
예제 #19
0
def _phrx_icmp4(self, packet_rx: PacketRx) -> None:
    """Handle inbound ICMPv4 packets"""

    Icmp4Parser(packet_rx)

    if packet_rx.parse_failed:
        if __debug__:
            log("icmp4",
                f"{packet_rx.tracker} - <CRIT>{packet_rx.parse_failed}</>")
        return

    if __debug__:
        log("icmp4", f"{packet_rx.tracker} - {packet_rx.icmp4}")

    # ICMPv4 Echo Request packet
    if packet_rx.icmp4.type == ICMP4_ECHO_REQUEST:
        if __debug__:
            log(
                "icmp4",
                f"{packet_rx.tracker} - <INFO>Received ICMPv4 Echo Request packet from {packet_rx.ip4.src}, sending reply</>"
            )

        self._phtx_icmp4(
            ip4_src=packet_rx.ip4.dst,
            ip4_dst=packet_rx.ip4.src,
            icmp4_type=ICMP4_ECHO_REPLY,
            icmp4_ec_id=packet_rx.icmp4.ec_id,
            icmp4_ec_seq=packet_rx.icmp4.ec_seq,
            icmp4_ec_data=packet_rx.icmp4.ec_data,
            echo_tracker=packet_rx.tracker,
        )
        return

    # ICMPv4 Unreachable packet
    if packet_rx.icmp4.type == ICMP4_UNREACHABLE:
        if __debug__:
            log(
                "icmp4",
                f"{packet_rx.tracker} - Received ICMPv4 Unreachable packet from {packet_rx.ip4.src}, will try to match UDP socket"
            )

        # Quick and dirty way to validate received data and pull useful information from it
        frame = packet_rx.icmp4.un_data
        if (len(frame) >= IP4_HEADER_LEN and frame[0] >> 4 == 4
                and len(frame) >= ((frame[0] & 0b00001111) << 2)
                and frame[9] == IP4_PROTO_UDP and len(frame) >=
            ((frame[0] & 0b00001111) << 2) + UDP_HEADER_LEN):
            # Create UdpMetadata object and try to find matching UDP socket
            udp_offset = (frame[0] & 0b00001111) << 2
            packet = UdpMetadata(
                local_ip_address=Ip4Address(frame[12:16]),
                remote_ip_address=Ip4Address(frame[16:20]),
                local_port=struct.unpack("!H", frame[udp_offset +
                                                     0:udp_offset + 2])[0],
                remote_port=struct.unpack("!H", frame[udp_offset +
                                                      2:udp_offset + 4])[0],
            )

            for socket_pattern in packet.socket_patterns:
                socket = stack.sockets.get(socket_pattern, None)
                if socket:
                    if __debug__:
                        log(
                            "icmp4",
                            f"{packet_rx.tracker} - <INFO>Found matching listening socket {socket}, for Unreachable packet from {packet_rx.ip4.src}</>",
                        )
                    socket.notify_unreachable()
                    return

            if __debug__:
                log(
                    "icmp4",
                    f"{packet_rx.tracker} - Unreachable data doesn't match any UDP socket"
                )
            return

        if __debug__:
            log(
                "icmp4",
                f"{packet_rx.tracker} - Unreachable data doesn't pass basic IPv4/UDP integrity check"
            )
        return
예제 #20
0
파일: ps.py 프로젝트: ccie18643/PyTCP
    def __init__(
        self,
        raw_packet: Optional[bytes] = None,
        dhcp_op: Optional[int] = None,
        dhcp_xid: Optional[int] = None,
        dhcp_flag_b: Optional[bool] = False,
        dhcp_ciaddr: Optional[Ip4Address] = None,
        dhcp_yiaddr: Optional[Ip4Address] = None,
        dhcp_siaddr: Optional[Ip4Address] = None,
        dhcp_giaddr: Optional[Ip4Address] = None,
        dhcp_chaddr: Optional[bytes] = None,
        dhcp_subnet_mask: Optional[Ip4Mask] = None,
        dhcp_router: Optional[list[Ip4Address]] = None,
        dhcp_dns: Optional[list[Ip4Address]] = None,
        dhcp_host_name: Optional[str] = None,
        dhcp_domain_name: Optional[str] = None,
        dhcp_req_ip_addr: Optional[Ip4Address] = None,
        dhcp_addr_lease_time: Optional[int] = None,
        dhcp_srv_id: Optional[Ip4Address] = None,
        dhcp_param_req_list: Optional[list[int]] = None,
        dhcp_msg_type: Optional[int] = None,
    ) -> None:
        """Class constructor"""

        # Packet parsing
        if raw_packet:
            raw_header = raw_packet[:DHCP4_HEADER_LEN]

            raw_options = raw_packet[DHCP4_HEADER_LEN:]

            self.dhcp_op = raw_header[0]
            self.dhcp_hwtype = raw_header[1]
            self.dhcp_hwlen = raw_header[2]
            self.dhcp_hops = raw_header[3]
            self.dhcp_xid = struct.unpack("!L", raw_header[4:8])[0]
            self.dhcp_secs = struct.unpack("!H", raw_header[8:10])[0]
            self.dhcp_flag_b = bool(
                struct.unpack("!H", raw_header[10:12])[0] & 0b1000000000000000)
            self.dhcp_ciaddr = Ip4Address(raw_header[12:16])
            self.dhcp_yiaddr = Ip4Address(raw_header[16:20])
            self.dhcp_siaddr = Ip4Address(raw_header[20:24])
            self.dhcp_giaddr = Ip4Address(raw_header[24:28])
            self.dhcp_chaddr = raw_header[28:28 + self.dhcp_hwlen]
            self.dhcp_sname = raw_header[44:108]
            self.dhcp_file = raw_header[108:236]

            self.dhcp_options: list[Dhcp4OptSubnetMask
                                    | Dhcp4OptRouter
                                    | Dhcp4OptDns
                                    | Dhcp4OptHostName
                                    | Dhcp4OptDomainName
                                    | Dhcp4OptReqIpAddr
                                    | Dhcp4OptAddrLeaseTime
                                    | Dhcp4OptParamReqList
                                    | Dhcp4OptSrvId
                                    | Dhcp4OptMsgType
                                    | Dhcp4OptPad
                                    | Dhcp4OptEnd] = []

            opt_cls = {
                DHCP4_OPT_SUBNET_MASK: Dhcp4OptSubnetMask,
                DHCP4_OPT_ROUTER: Dhcp4OptRouter,
                DHCP4_OPT_DNS: Dhcp4OptDns,
                DHCP4_OPT_HOST_NAME: Dhcp4OptHostName,
                DHCP4_OPT_DOMAIN_NAME: Dhcp4OptDomainName,
                DHCP4_OPT_REQ_IP4_ADDR: Dhcp4OptReqIpAddr,
                DHCP4_OPT_ADDR_LEASE_TIME: Dhcp4OptAddrLeaseTime,
                DHCP4_OPT_PARAM_REQ_LIST: Dhcp4OptParamReqList,
                DHCP4_OPT_SRV_ID: Dhcp4OptSrvId,
                DHCP4_OPT_MSG_TYPE: Dhcp4OptMsgType,
            }

            i = 0

            while i < len(raw_options):

                if raw_options[i] == DHCP4_OPT_END:
                    self.dhcp_options.append(Dhcp4OptEnd())
                    break

                if raw_options[i] == DHCP4_OPT_PAD:
                    self.dhcp_options.append(Dhcp4OptPad())
                    i += DHCP4_OPT_PAD_LEN
                    continue

                self.dhcp_options.append(
                    opt_cls.get(raw_options[i], Dhcp4OptUnk)(
                        raw_options[i:i + raw_options[i + 1] + 2]))
                i += self.raw_options[i + 1] + 2

        # Packet building
        else:
            assert dhcp_op is not None
            assert dhcp_xid is not None
            assert dhcp_flag_b is not None
            assert dhcp_ciaddr is not None
            assert dhcp_yiaddr is not None
            assert dhcp_siaddr is not None
            assert dhcp_giaddr is not None
            assert dhcp_chaddr is not None

            self.dhcp_op = dhcp_op
            self.dhcp_hwtype = 1
            self.dhcp_hwlen = 6
            self.dhcp_hops = 0
            self.dhcp_xid = dhcp_xid
            self.dhcp_secs = 0
            self.dhcp_flag_b = dhcp_flag_b
            self.dhcp_ciaddr = dhcp_ciaddr
            self.dhcp_yiaddr = dhcp_yiaddr
            self.dhcp_siaddr = dhcp_siaddr
            self.dhcp_giaddr = dhcp_giaddr
            self.dhcp_chaddr = dhcp_chaddr
            self.dhcp_sname = b"\0" * 64
            self.dhcp_file = b"\0" * 128

            self.dhcp_options = []

            if dhcp_subnet_mask:
                self.dhcp_options.append(
                    Dhcp4OptSubnetMask(opt_subnet_mask=dhcp_subnet_mask))

            if dhcp_router:
                self.dhcp_options.append(
                    Dhcp4OptRouter(opt_router=dhcp_router))

            if dhcp_dns:
                self.dhcp_options.append(Dhcp4OptDns(opt_dns=dhcp_dns))

            if dhcp_host_name:
                self.dhcp_options.append(
                    Dhcp4OptHostName(opt_host_name=dhcp_host_name))

            if dhcp_domain_name:
                self.dhcp_options.append(
                    Dhcp4OptDomainName(opt_domain_name=dhcp_domain_name))

            if dhcp_req_ip_addr:
                self.dhcp_options.append(
                    Dhcp4OptReqIpAddr(opt_req_ip_addr=dhcp_req_ip_addr))

            if dhcp_addr_lease_time:
                self.dhcp_options.append(
                    Dhcp4OptAddrLeaseTime(
                        opt_addr_lease_time=dhcp_addr_lease_time))

            if dhcp_srv_id:
                self.dhcp_options.append(Dhcp4OptSrvId(opt_srv_id=dhcp_srv_id))

            if dhcp_param_req_list:
                self.dhcp_options.append(
                    Dhcp4OptParamReqList(
                        opt_param_req_list=dhcp_param_req_list))

            if dhcp_msg_type:
                self.dhcp_options.append(
                    Dhcp4OptMsgType(opt_msg_type=dhcp_msg_type))

            self.dhcp_options.append(Dhcp4OptEnd())