diff --git a/README.md b/README.md index 94924195..33fb05e0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PyTCP (version 2.0) +# PyTCP (version 2.2) PyTCP is an attempt to create fully functional TCP/IP stack in Python. It supports TCP stream based transport with reliable packet delivery based on sliding window mechanism and basic congestion control. It also supports IPv6/ICMPv6 protocols with SLAAC address configuration. It operates as user space program attached to Linux TAP interface. As of today stack is able to send and receive traffic over Internet using IPv4 and IPv6 default gateways for routing. @@ -51,8 +51,9 @@ I am also working on another TCP/IP stack project that is being programmed in C - QUIC protocol - *research and plan for the implementation, this depends on ability to create lab environment for it* - IPv6 protocol - *redesign the RA PI option handling and ND prefix auto configuration to properly use A nad L flags, some research also needed on case when different than /64 prefix is being advertised* - - IPv6 protocol - *implement remaining extended headers* + - IPv6 protocol - *implement optional headers* - IPv6 protocol - *validate and possibly re-implements certain IPv6 mechanisms/processes according to RFC rules* + - IPv6 protocol - *research and possibly implement support for certain optional IPv6 headers* - IPv6 protocol - *investigate Hop-by-Hop Options header and its relation to MLD2 Report message, implement if needed for MLD2 to work properly* - ICMPv6 protocol - *validate and possibly re-implements certain IPv6 mechanisms/processes according to RFC rules* - ICMPv6 protocol - *implement ND Redirect message* @@ -62,16 +63,17 @@ I am also working on another TCP/IP stack project that is being programmed in C - TCP protocol - *proper handling on RST packets in various states, need to do research on this* - TCP protocol - *need to rework the CLOSE syscall mechanism so FIN flag can be set on last data packet instead of being carried in separate one* - TCP protocol - *ACK packet retransmission in case we got FIN retransmission in TIME_WAIT state <-- need to investigate this* - - TCP protocol - *implement proper response to packets containing old SEQ and/or ACK numbers <-- need to investigate this* + - TCP protocol - *implement proper response to packets containing old SEQ and/or ACK numbersi <-- need to investigate this* - TCP protocol - *ensure that event communication from TCP session to socket works properly (eg. connection reset by peer)* - ICMP protocols - *need to come up with some sort of "icmp socket" mechanism so ping client can bind to particular ICMP echo-reply stream* + - IPv4 protocol - *improvements in IP defragmentation mechanism are needed, out of order fragment handling, purging of orphaned fragments* - IPv6/IPv4 protocols - *proper routing mechanism, route tables, etc...* - IPv6/IPv4 protocols - *ability of stack to act as a router* - ARP cache - *implement proper FSM* - ICMPv6 ND cache - *implement proper FSM* - UDP protocol - *need UDP echo client and mechanism to report receiving ICMP Port Unreachable message to UDP socket* - - UDP sockets - *overhaul is needed to make 'end user' interface match Berkeley sockets interface 100% so 3rd party aps can use it without porting* - - TCP sockets - *overhaul is needed to make 'end user' interface match Berkeley sockets interface 100% so 3rd party aps can use it without porting* + - UDP sockets - *overhaul is needed to make 'end user' interface match Berkeley sockets more closely so 3rd party aps can use it without porting* + - TCP sockets - *overhaul is needed to make 'end user' interface match Berkeley sockets more closely so 3rd party aps can use it without porting* ### Examples: diff --git a/arp/__init__.py b/arp/__init__.py new file mode 100755 index 00000000..7fa27ec9 --- /dev/null +++ b/arp/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# arp/__init__.py +# diff --git a/arp/fpa.py b/arp/fpa.py new file mode 100755 index 00000000..9ae9a871 --- /dev/null +++ b/arp/fpa.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# arp/fpa.py - Fast Packet Assembler support class for ARP protocol +# + + +import struct +from typing import Optional + +import arp.ps +import ether.ps +from misc.ipv4_address import IPv4Address +from misc.tracker import Tracker + + +class Assembler: + """ARP packet assembler support class""" + + ether_type = ether.ps.TYPE_ARP + + def __init__( + self, + sha: str, + spa: IPv4Address, + tpa: IPv4Address, + tha: str = "00:00:00:00:00:00", + oper: int = arp.ps.OP_REQUEST, + echo_tracker: Optional[Tracker] = None, + ) -> None: + """Class constructor""" + + self.tracker = Tracker("TX", echo_tracker) + + self.hrtype = 1 + self.prtype = 0x0800 + self.hrlen = 6 + self.prlen = 4 + self.oper = oper + self.sha = sha + self.spa = IPv4Address(spa) + self.tha = tha + self.tpa = IPv4Address(tpa) + + def __len__(self) -> int: + """Length of the packet""" + + return arp.ps.HEADER_LEN + + from arp.ps import __str__ + + def assemble(self, frame: bytearray, hptr: int): + """Assemble packet into the raw form""" + + return struct.pack_into( + "!HH BBH 6s 4s 6s 4s", + frame, + hptr, + self.hrtype, + self.prtype, + self.hrlen, + self.prlen, + self.oper, + bytes.fromhex(self.sha.replace(":", "")), + IPv4Address(self.spa).packed, + bytes.fromhex(self.tha.replace(":", "")), + IPv4Address(self.tpa).packed, + ) diff --git a/arp/fpp.py b/arp/fpp.py new file mode 100755 index 00000000..cc54bc28 --- /dev/null +++ b/arp/fpp.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# arp/fpp.py - Fast Packet Parser support class for ARP protocol +# + + +import struct + +import arp.ps +import config +from misc.ipv4_address import IPv4Address +from misc.packet import PacketRx + + +class Parser: + """ARP packet parser class""" + + def __init__(self, packet_rx: PacketRx) -> None: + """Class constructor""" + + packet_rx.arp = self + + self._frame = packet_rx.frame + self._hptr = packet_rx.hptr + + packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() + + def __len__(self) -> int: + """Number of bytes remaining in the frame""" + + return len(self._frame) - self._hptr + + from arp.ps import __str__ + + @property + def hrtype(self) -> int: + """Read 'Hardware address type' field""" + + if "_cache__hrtype" not in self.__dict__: + self._cache__hrtype = struct.unpack_from("!H", self._frame, self._hptr + 0)[0] + return self._cache__hrtype + + @property + def prtype(self) -> int: + """Read 'Protocol address type' field""" + + if "_cache__prtype" not in self.__dict__: + self._cache__prtype = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] + return self._cache__prtype + + @property + def hrlen(self) -> int: + """Read 'Hardware address length' field""" + + return self._frame[self._hptr + 4] + + @property + def prlen(self) -> int: + """Read 'Protocol address length' field""" + + return self._frame[self._hptr + 5] + + @property + def oper(self) -> int: + """Read 'Operation' field""" + + if "_cache__oper" not in self.__dict__: + self._cache__oper = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] + return self._cache__oper + + @property + def sha(self) -> str: + """Read 'Sender hardware address' field""" + + if "_cache__sha" not in self.__dict__: + self._cache__sha = ":".join([f"{_:0>2x}" for _ in self._frame[self._hptr + 8 : self._hptr + 14]]) + return self._cache__sha + + @property + def spa(self) -> IPv4Address: + """Read 'Sender protocol address' field""" + + if "_cache__spa" not in self.__dict__: + self._cache__spa = IPv4Address(self._frame[self._hptr + 14 : self._hptr + 18]) + return self._cache__spa + + @property + def tha(self) -> str: + """Read 'Target hardware address' field""" + + if "_cache__tha" not in self.__dict__: + self._cache__tha = ":".join([f"{_:0>2x}" for _ in self._frame[self._hptr + 18 : self._hptr + 24]]) + return self._cache__tha + + @property + def tpa(self) -> IPv4Address: + """Read 'Target protocol address' field""" + + if "_cache__tpa" not in self.__dict__: + self._cache__tpa = IPv4Address(self._frame[self._hptr + 24 : self._hptr + 28]) + return self._cache__tpa + + @property + def packet_copy(self) -> bytes: + """Read the whole packet""" + + if "_cache__packet_copy" not in self.__dict__: + self._cache__packet_copy = self._frame[self._hptr : self._hptr + arp.ps.HEADER_LEN] + return self._cache__packet_copy + + def _packet_integrity_check(self) -> str: + """Packet integrity check to be run on raw packet prior to parsing to make sure parsing is safe""" + + if not config.packet_integrity_check: + return "" + + if len(self) < arp.ps.HEADER_LEN: + return "ARP integrity - wrong packet length (I)" + + return "" + + def _packet_sanity_check(self) -> str: + """Packet sanity check to be run on parsed packet to make sure packet's fields contain sane values""" + + if not config.packet_sanity_check: + return "" + + if self.hrtype != 1: + return "ARP sanity - 'arp_hrtype' must be 1" + + if self.prtype != 0x0800: + return "ARP sanity - 'arp_prtype' must be 0x0800" + + if self.hrlen != 6: + return "ARP sanity - 'arp_hrlen' must be 6" + + if self.prlen != 4: + return "ARP sanity - 'arp_prlen' must be 4" + + if self.oper not in {1, 2}: + return "ARP sanity - 'oper' must be [1-2]" + + return "" diff --git a/phrx_arp.py b/arp/phrx.py similarity index 78% rename from phrx_arp.py rename to arp/phrx.py index a131c15f..6fb47904 100755 --- a/phrx_arp.py +++ b/arp/phrx.py @@ -23,33 +23,27 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phrx_arp.py - packet handler for inbound ARP packets +# arp/phrx.py - packet handler for inbound ARP packets # +from typing import cast + +import arp.fpp +import arp.ps import config -import fpp_arp -from ipv4_address import IPv4Address +from arp.fpp import Parser as ArpParser +from ether.fpp import Parser as EtherParser +from misc.ipv4_address import IPv4Address +from misc.packet import PacketRx -def _phrx_arp(self, packet_rx): +def _phrx_arp(self, packet_rx: PacketRx) -> None: """Handle inbound ARP packets""" - fpp_arp.ArpPacket(packet_rx) + arp.fpp.Parser(packet_rx) if packet_rx.parse_failed: if __debug__: @@ -59,7 +53,9 @@ def _phrx_arp(self, packet_rx): if __debug__: self._logger.opt(ansi=True).info(f"{packet_rx.tracker} - {packet_rx.arp}") - if packet_rx.arp.oper == fpp_arp.ARP_OP_REQUEST: + packet_rx.arp = cast(ArpParser, packet_rx.arp) + + if packet_rx.arp.oper == arp.ps.OP_REQUEST: # Check if request contains our IP address in SPA field, this indicates IP address conflict if packet_rx.arp.spa in self.ip4_unicast: if __debug__: @@ -71,7 +67,7 @@ def _phrx_arp(self, packet_rx): self._phtx_arp( ether_src=self.mac_unicast, ether_dst=packet_rx.arp.sha, - arp_oper=fpp_arp.ARP_OP_REPLY, + arp_oper=arp.ps.OP_REPLY, arp_sha=self.mac_unicast, arp_spa=packet_rx.arp.tpa, arp_tha=packet_rx.arp.sha, @@ -88,7 +84,8 @@ def _phrx_arp(self, packet_rx): return # Handle ARP reply - elif packet_rx.arp.oper == fpp_arp.ARP_OP_REPLY: + elif packet_rx.arp.oper == arp.ps.OP_REPLY: + packet_rx.ether = cast(EtherParser, packet_rx.ether) # Check for ARP reply that is response to our ARP probe, that indicates that IP address we trying to claim is in use if packet_rx.ether.dst == self.mac_unicast: if ( diff --git a/phtx_arp.py b/arp/phtx.py similarity index 63% rename from phtx_arp.py rename to arp/phtx.py index a3ef11f8..c0f7cd4b 100755 --- a/phtx_arp.py +++ b/arp/phtx.py @@ -23,36 +23,28 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phtx_arp.py - packet handler for outbound ARP packets +# arp/phtx.py - packet handler for outbound ARP packets # +import arp.fpa import config -import fpa_arp +from misc.ipv4_address import IPv4Address +from misc.tracker import Tracker -def _phtx_arp(self, ether_src, ether_dst, arp_oper, arp_sha, arp_spa, arp_tha, arp_tpa, echo_tracker=None): +def _phtx_arp( + self, ether_src: str, ether_dst: str, arp_oper: int, arp_sha: str, arp_spa: IPv4Address, arp_tha: str, arp_tpa: IPv4Address, echo_tracker: Tracker = None +) -> None: """Handle outbound ARP packets""" # Check if IPv4 protocol support is enabled, if not then silently drop the packet if not config.ip4_support: return - arp_packet_tx = fpa_arp.ArpPacket( + arp_packet_tx = arp.fpa.Assembler( oper=arp_oper, sha=arp_sha, spa=arp_spa, @@ -64,4 +56,4 @@ def _phtx_arp(self, ether_src, ether_dst, arp_oper, arp_sha, arp_spa, arp_tha, a if __debug__: self._logger.opt(ansi=True).info(f"{arp_packet_tx.tracker} - {arp_packet_tx}") - self._phtx_ether(ether_src=ether_src, ether_dst=ether_dst, child_packet=arp_packet_tx) + self._phtx_ether(ether_src=ether_src, ether_dst=ether_dst, carried_packet=arp_packet_tx) diff --git a/arp/ps.py b/arp/ps.py new file mode 100755 index 00000000..c0dbeb7f --- /dev/null +++ b/arp/ps.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# arp/ps.py - protocol support for ARP +# + + +# ARP packet header - IPv4 stack version only + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Hardware type | Protocol type | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Hard length | Proto length | Operation | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | > +# + Sender MAC address +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# > | Sender IP address > +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# > | > +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Target MAC address | +# > | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Target IP address | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +HEADER_LEN = 28 + +OP_REQUEST = 1 +OP_REPLY = 2 + + +def __str__(self) -> str: + """Packet log string""" + + if self.oper == OP_REQUEST: + return f"ARP request {self.spa} / {self.sha} > {self.tpa} / {self.tha}" + if self.oper == OP_REPLY: + return f"ARP reply {self.spa} / {self.sha} > {self.tpa} / {self.tha}" + + return f"ARP request unknown operation {self.oper}" diff --git a/client/__init__.py b/client/__init__.py new file mode 100755 index 00000000..1bc53f5e --- /dev/null +++ b/client/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# client/__init__.py +# diff --git a/client_icmp_echo.py b/client/icmp_echo.py similarity index 55% rename from client_icmp_echo.py rename to client/icmp_echo.py index 579e65a2..5f127170 100755 --- a/client_icmp_echo.py +++ b/client/icmp_echo.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# client_icmp_echo.py - 'user space' client for ICMPv4/v6 echo +# client/icmp_echo.py - 'user space' client for ICMPv4/v6 echo # @@ -46,34 +34,35 @@ import time from datetime import datetime -import stack -from ip_helper import ip_pick_version +import misc.stack as stack +from misc.ip_helper import ip_pick_version class ClientIcmpEcho: """ICMPv4/v6 Echo client support class""" - def __init__(self, local_ip_address, remote_ip_address, message_count=None): + def __init__(self, local_ip_address: str, remote_ip_address: str, message_count: int = -1) -> None: """Class constructor""" - local_ip_address = ip_pick_version(local_ip_address) - remote_ip_address = ip_pick_version(remote_ip_address) + self.local_ip_address = ip_pick_version(local_ip_address) + self.remote_ip_address = ip_pick_version(remote_ip_address) + self.message_count = message_count - threading.Thread(target=self.__thread_client, args=(local_ip_address, remote_ip_address, message_count)).start() + threading.Thread(target=self.__thread_client).start() - @staticmethod - def __thread_client(local_ip_address, remote_ip_address, message_count): + def __thread_client(self) -> None: flow_id = random.randint(0, 65535) + message_count = self.message_count message_seq = 0 - while message_count is None or message_seq < message_count: + while message_count: message = bytes(str(datetime.now()) + "\n", "utf-8") - if local_ip_address.version == 4: - stack.packet_handler.phtx_icmp4( - ip4_src=local_ip_address, - ip4_dst=remote_ip_address, + if self.local_ip_address.version == 4: + stack.packet_handler._phtx_icmp4( # type: ignore + ip4_src=self.local_ip_address, + ip4_dst=self.remote_ip_address, icmp4_type=8, icmp4_code=0, icmp4_ec_id=flow_id, @@ -81,10 +70,10 @@ def __thread_client(local_ip_address, remote_ip_address, message_count): icmp4_ec_data=message, ) - if local_ip_address.version == 6: - stack.packet_handler.phtx_icmp6( - ip6_src=local_ip_address, - ip6_dst=remote_ip_address, + if self.local_ip_address.version == 6: + stack.packet_handler._phtx_icmp6( # type: ignore + ip6_src=self.local_ip_address, + ip6_dst=self.remote_ip_address, icmp6_type=128, icmp6_code=0, icmp6_ec_id=flow_id, @@ -92,6 +81,7 @@ def __thread_client(local_ip_address, remote_ip_address, message_count): icmp6_ec_data=message, ) - print(f"Client ICMP Echo: Sent ICMP Echo ({flow_id}/{message_seq}) to {remote_ip_address} - {message}") + print(f"Client ICMP Echo: Sent ICMP Echo ({flow_id}/{message_seq}) to {self.remote_ip_address} - {str(message)}") time.sleep(1) message_seq += 1 + message_count = min(message_count, message_count - 1) diff --git a/client/tcp_echo.py b/client/tcp_echo.py new file mode 100755 index 00000000..9870150a --- /dev/null +++ b/client/tcp_echo.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# client/tcp_echo.py - 'user space' client for TCP echo, it activelly connects to service and sends messages +# + + +import threading +import time +from datetime import datetime + +from misc.ip_helper import ip_pick_version +from tcp.socket import TcpSocket + + +class ClientTcpEcho: + """TCP Echo client support class""" + + def __init__(self, local_ip_address: str, remote_ip_address: str, local_port: int = 0, remote_port: int = 7, message_count: int = -1) -> None: + """Class constructor""" + + self.local_ip_address = ip_pick_version(local_ip_address) + self.remote_ip_address = ip_pick_version(remote_ip_address) + self.local_port = local_port + self.remote_port = remote_port + self.message_count = message_count + + threading.Thread(target=self.__thread_client).start() + + def __thread_client(self) -> None: + socket = TcpSocket() + socket.bind(self.local_ip_address, self.local_port) + + print(f"Client TCP Echo: opening connection to {self.remote_ip_address}, port {self.remote_port}") + if socket.connect(remote_ip_address=self.remote_ip_address, remote_port=self.remote_port): + print(f"Client TCP Echo: Connection to {self.remote_ip_address}, port {self.remote_port} has been established") + else: + print(f"Client TCP Echo: Connection to {self.remote_ip_address}, port {self.remote_port} failed") + return + + message_count = self.message_count + while message_count: + message = bytes(str(datetime.now()) + "\n", "utf-8") + # message = bytes("***START***" + "1234567890" * 1000 + "***STOP***", "utf-8") + if socket.send(message): + print(f"Client TCP Echo: Sent data to {self.remote_ip_address}, port {self.remote_port} - {str(message)}") + time.sleep(1) + message_count = min(message_count, message_count - 1) + else: + print(f"Client TCP Echo: Peer {self.remote_ip_address}, port {self.remote_port} closed connection") + break + + socket.close() + print(f"Client TCP Echo: Closed connection to {self.remote_ip_address}, port {self.remote_port}") diff --git a/client_tcp_echo.py b/client_tcp_echo.py deleted file mode 100755 index 879d93e2..00000000 --- a/client_tcp_echo.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# client_tcp_echo.py - 'user space' client for TCP echo, it activelly connects to service and sends messages -# - - -import threading -import time -from datetime import datetime - -from ip_helper import ip_pick_version -from tcp_socket import TcpSocket - - -class ClientTcpEcho: - """TCP Echo client support class""" - - def __init__(self, local_ip_address, remote_ip_address, local_port=0, remote_port=7, message_count=10): - """Class constructor""" - - local_ip_address = ip_pick_version(local_ip_address) - remote_ip_address = ip_pick_version(remote_ip_address) - - threading.Thread(target=self.__thread_client, args=(local_ip_address, local_port, remote_ip_address, remote_port, message_count)).start() - - @staticmethod - def __thread_client(local_ip_address, local_port, remote_ip_address, remote_port, message_count): - socket = TcpSocket() - socket.bind(local_ip_address, local_port) - - print(f"Client TCP Echo: opening connection to {remote_ip_address}, port {remote_port}") - if socket.connect(remote_ip_address=remote_ip_address, remote_port=remote_port): - print(f"Client TCP Echo: Connection to {remote_ip_address}, port {remote_port} has been established") - else: - print(f"Client TCP Echo: Connection to {remote_ip_address}, port {remote_port} failed") - return - - i = 1 - while i <= message_count: - message = bytes(str(datetime.now()) + "\n", "utf-8") - # message = bytes("***START***" + "1234567890" * 1000 + "***STOP***", "utf-8") - if socket.send(message): - print(f"Client TCP Echo: Sent data to {remote_ip_address}, port {remote_port} - {message}") - time.sleep(1) - i += 1 - else: - print(f"Client TCP Echo: Peer {remote_ip_address}, port {remote_port} closed connection") - break - - socket.close() - print(f"Client TCP Echo: Closed connection to {remote_ip_address}, port {remote_port}") diff --git a/config.py b/config.py index 0827bd25..194d9a67 100755 --- a/config.py +++ b/config.py @@ -23,26 +23,11 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # # config.py - module contains global configuration parameters # -from sys import version_info - -assert version_info >= (3, 9), "PyTCP requires Python version 3.9 or higher" # TAP interface name stack should bind itself to interface = b"tap7" @@ -89,16 +74,8 @@ # Basic routing is implemented and each subnet can have its own gateway # Link local addresses should have default gateway set to 'None' ip6_address_candidate = [ - ("FE80::7/64", None), - # ("FE80::77/64", None), - # ("FE80::7777/64", None), - # ("FE80::7777/64", None), # test duplicate address - # ("FE80::9999/64", "FE80::1"), # test link local address with default gateway - # ("2007::1111/64", "DUPA"), # test link local address with default gateway - # ("ZHOPA", None), # test invalid address - # ("2099::99/64", "2009::99"), # test invalid gateway + ("FE80::7/64", ""), # ("2007::7/64", "FE80::1"), - # ("2009::9/64", "2009::1"), ] # IPv4 DHCP based address configuration @@ -110,31 +87,25 @@ ip4_address_candidate = [ ("192.168.9.7/24", "192.168.9.1"), # ("192.168.9.77/24", "192.168.9.1"), - # ("224.0.0.1/24", "192.168.9.1"), # test invalid address type - # ("DUPA", "192.168.9.1"), # test invalid address format - # ("192.168.9.99/24", "DUPA"), # test invalid gateway format - # ("192.168.9.77/24", "192.168.9.1"), # test duplicate address - # ("192.168.9.170/24", "10.0.0.1"), # test invalid gateway - # ("192.168.9.171/24", "192.168.9.0"), # test invalid gateway - # ("192.168.9.172/24", "192.168.9.172"), # test invalid gateway - # ("192.168.9.173/24", "192.168.9.255"), # test invalid gateway - # ("192.168.9.0/24", "192.168.9.1"), # test invalid address - # ("192.168.9.255/24", "192.168.9.1"), # test invalid address - # ("0.0.0.0/0", None), # test invalid address - # ("192.168.9.102/24", None), # test no gateway # ("172.16.17.7/24", "172.16.17.1"), # ("10.10.10.7/24", "10.10.10.1"), ] # ARP cache configuration +arp_cache_entry_max_age = 3600 +arp_cache_entry_refresh_time = 300 arp_cache_update_from_direct_request = True arp_cache_update_from_gratuitious_reply = True +# ICMPv6 ND cache configuration +nd_cache_entry_max_age = 3600 +nd_cache_entry_refresh_time = 300 + # TCP ephemeral port range to be used by outbound connections -TCP_EPHEMERAL_PORT_RANGE = (32168, 60999) +tcp_ephemeral_port_range = (32168, 60999) # UDP ephemeral port range to be used by outbound connections -UDP_EPHEMERAL_PORT_RANGE = (32168, 60999) +udp_ephemeral_port_range = (32168, 60999) # TAP interface MTU, describes how much payload Ethernet packet can carry mtu = 1500 @@ -146,11 +117,11 @@ # Those are being used for testing various stack components are therefore their 'default' funcionality may be altered for specific test needs # Eg. TCP daytime service generates large amount of text data used to verify TCP protocol funcionality service_udp_echo = True -service_udp_discard = True -service_udp_daytime = True +service_udp_discard = False +service_udp_daytime = False service_tcp_echo = True -service_tcp_discard = True -service_tcp_daytime = True +service_tcp_discard = False +service_tcp_daytime = False # For using test clients proper IP addressing needs to be set up in file 'pytcp.py' client_tcp_echo = False diff --git a/dhcp4/__init__.py b/dhcp4/__init__.py new file mode 100755 index 00000000..0b02ed5f --- /dev/null +++ b/dhcp4/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# dhcp4/__init__.py +# diff --git a/dhcp4/client.py b/dhcp4/client.py new file mode 100755 index 00000000..5d6961b0 --- /dev/null +++ b/dhcp4/client.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# dhcp4/client.py - DHCPv4 client function for packet handler +# + + +import random + +import dhcp4.ps +import udp.metadata +import udp.socket +from misc.ipv4_address import IPv4Address, IPv4Interface + + +def _dhcp4_client(self): + """IPv4 DHCP client""" + + def _send_dhcp_packet(dhcp_packet_tx): + socket.send_to( + udp.metadata.UdpMetadata( + local_ip_address=IPv4Address("0.0.0.0"), + local_port=68, + remote_ip_address=IPv4Address("255.255.255.255"), + remote_port=67, + data=dhcp_packet_tx.get_raw_packet(), + ) + ) + + socket = udp.socket.UdpSocket() + socket.bind(local_ip_address="0.0.0.0", local_port=68) + dhcp_xid = random.randint(0, 0xFFFFFFFF) + + # Send DHCP Discover + _send_dhcp_packet( + dhcp_packet_tx=dhcp4.ps.Packet( + dhcp_xid=dhcp_xid, + dhcp_chaddr=self.mac_unicast, + dhcp_msg_type=dhcp4.ps.MSG_DISCOVER, + dhcp_param_req_list=b"\x01\x1c\x02\x03\x0f\x06\x77\x0c\x2c\x2f\x1a\x79\x2a", + dhcp_host_name="PyTCP", + ) + ) + if __debug__: + self._logger.debug("Sent out DHCP Discover message") + + # Wait for DHCP Offer + if not (packet := socket.receive_from(timeout=5)): + if __debug__: + self._logger.warning("Timeout waiting for DHCP Offer message") + socket.close() + return None, None + + dhcp_packet_rx = dhcp4.ps.Packet(packet.data) + if dhcp_packet_rx.dhcp_msg_type != dhcp4.ps.MSG_OFFER: + if __debug__: + self._logger.warning("Didn't receive DHCP Offer message") + socket.close() + return None, None + + dhcp_srv_id = dhcp_packet_rx.dhcp_srv_id + dhcp_yiaddr = dhcp_packet_rx.dhcp_yiaddr + if __debug__: + self._logger.debug( + 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 + _send_dhcp_packet( + dhcp_packet_tx=dhcp4.ps.Packet( + dhcp_xid=dhcp_xid, + dhcp_chaddr=self.mac_unicast, + dhcp_msg_type=dhcp4.ps.MSG_REQUEST, + dhcp_srv_id=dhcp_srv_id, + dhcp_req_ip4_addr=dhcp_yiaddr, + dhcp_param_req_list=b"\x01\x1c\x02\x03\x0f\x06\x77\x0c\x2c\x2f\x1a\x79\x2a", + dhcp_host_name="PyTCP", + ) + ) + + if __debug__: + self._logger.debug(f"Sent out DHCP Request message to {dhcp_packet_rx.dhcp_srv_id}") + + # Wait for DHCP Ack + if not (packet := socket.receive_from(timeout=5)): + if __debug__: + self._logger.warning("Timeout waiting for DHCP Ack message") + return None, None + + dhcp_packet_rx = dhcp4.ps.Packet(packet.data) + if dhcp_packet_rx.dhcp_msg_type != dhcp4.ps.MSG_ACK: + if __debug__: + self._logger.warning("Didn't receive DHCP Offer message") + socket.close() + return None, None + + if __debug__: + self._logger.debug( + 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}" + ) + socket.close() + return ( + IPv4Interface(str(dhcp_packet_rx.dhcp_yiaddr) + "/" + str(IPv4Address._make_netmask(str(dhcp_packet_rx.dhcp_subnet_mask))[1])), + dhcp_packet_rx.dhcp_router[0], + ) diff --git a/ps_dhcp.py b/dhcp4/ps.py similarity index 78% rename from ps_dhcp.py rename to dhcp4/ps.py index 47e9f0a8..a12f53a5 100755 --- a/ps_dhcp.py +++ b/dhcp4/ps.py @@ -23,28 +23,16 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# fpa_dhcp.py - protocol support library for DHCP +# dhcp4/ps.py - protocol support library for DHCP # import binascii import struct -from ipv4_address import IPv4Address +from misc.ipv4_address import IPv4Address # DHCP packet header (RFC 2131) @@ -122,22 +110,22 @@ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -DHCP_HEADER_LEN = 236 + 4 +HEADER_LEN = 236 + 4 -BOOT_REQUEST = 1 -BOOT_REPLY = 2 +OP_REQUEST = 1 +OP_REPLY = 2 -DHCP_DISCOVER = 1 -DHCP_OFFER = 2 -DHCP_REQUEST = 3 -DHCP_DECLINE = 4 -DHCP_ACK = 5 -DHCP_NAK = 6 -DHCP_RELEASE = 7 -DHCP_INFORM = 8 +MSG_DISCOVER = 1 +MSG_OFFER = 2 +MSG_REQUEST = 3 +MSG_DECLINE = 4 +MSG_ACK = 5 +MSG_NAK = 6 +MSG_RELEASE = 7 +MSG_INFORM = 8 -class DhcpPacket: +class Packet: """Dhcp packet support class""" protocol = "DHCP" @@ -145,7 +133,7 @@ class DhcpPacket: def __init__( self, raw_packet=None, - dhcp_op=BOOT_REQUEST, + dhcp_op=OP_REQUEST, dhcp_xid=None, dhcp_flag_b=False, dhcp_ciaddr=IPv4Address("0.0.0.0"), @@ -168,9 +156,9 @@ def __init__( # Packet parsing if raw_packet: - raw_header = raw_packet[:DHCP_HEADER_LEN] + raw_header = raw_packet[:HEADER_LEN] - raw_options = raw_packet[DHCP_HEADER_LEN:] + raw_options = raw_packet[HEADER_LEN:] self.dhcp_op = raw_header[0] self.dhcp_htype = raw_header[1] @@ -190,32 +178,32 @@ def __init__( self.dhcp_options = [] opt_cls = { - DHCP_OPT_SUBNET_MASK: DhcpOptSubnetMask, - DHCP_OPT_ROUTER: DhcpOptRouter, - DHCP_OPT_DNS: DhcpOptDns, - DHCP_OPT_HOST_NAME: DhcpOptHostName, - DHCP_OPT_DOMAIN_NAME: DhcpOptDomainName, - DHCP_OPT_REQ_IP4_ADDR: DhcpOptReqIpAddr, - DHCP_OPT_ADDR_LEASE_TIME: DhcpOptAddrLeaseTime, - DHCP_OPT_PARAM_REQ_LIST: DhcpOptParamReqList, - DHCP_OPT_SRV_ID: DhcpOptSrvId, - DHCP_OPT_MSG_TYPE: DhcpOptMsgType, + OPT_SUBNET_MASK: OptSubnetMask, + OPT_ROUTER: OptRouter, + OPT_DNS: OptDns, + OPT_HOST_NAME: OptHostName, + OPT_DOMAIN_NAME: OptDomainName, + OPT_REQ_IP4_ADDR: OptReqIpAddr, + OPT_ADDR_LEASE_TIME: OptAddrLeaseTime, + OPT_PARAM_REQ_LIST: OptParamReqList, + OPT_SRV_ID: OptSrvId, + OPT_MSG_TYPE: OptMsgType, } i = 0 while i < len(raw_options): - if raw_options[i] == DHCP_OPT_END: - self.dhcp_options.append(DhcpOptEnd()) + if raw_options[i] == OPT_END: + self.dhcp_options.append(OptEnd()) break - if raw_options[i] == DHCP_OPT_PAD: - self.dhcp_options.append(DhcpOptPad()) - i += DHCP_OPT_PAD_LEN + if raw_options[i] == OPT_PAD: + self.dhcp_options.append(OptPad()) + i += OPT_PAD_LEN continue - self.dhcp_options.append(opt_cls.get(raw_options[i], DhcpOptUnk)(raw_options[i : i + raw_options[i + 1] + 2])) + self.dhcp_options.append(opt_cls.get(raw_options[i], OptUnk)(raw_options[i : i + raw_options[i + 1] + 2])) i += self.raw_options[i + 1] + 2 # Packet building @@ -238,36 +226,36 @@ def __init__( self.dhcp_options = [] if dhcp_subnet_mask: - self.dhcp_options.append(DhcpOptSubnetMask(opt_subnet_mask=dhcp_subnet_mask)) + self.dhcp_options.append(OptSubnetMask(opt_subnet_mask=dhcp_subnet_mask)) if dhcp_router: - self.dhcp_options.append(DhcpOptRouter(opt_router=dhcp_router)) + self.dhcp_options.append(OptRouter(opt_router=dhcp_router)) if dhcp_dns: - self.dhcp_options.append(DhcpOptDns(opt_dns=dhcp_dns)) + self.dhcp_options.append(OptDns(opt_dns=dhcp_dns)) if dhcp_host_name: - self.dhcp_options.append(DhcpOptHostName(opt_host_name=dhcp_host_name)) + self.dhcp_options.append(OptHostName(opt_host_name=dhcp_host_name)) if dhcp_domain_name: - self.dhcp_options.append(DhcpOptDomainName(opt_domain_name=dhcp_domain_name)) + self.dhcp_options.append(OptDomainName(opt_domain_name=dhcp_domain_name)) if dhcp_req_ip4_addr: - self.dhcp_options.append(DhcpOptReqIpAddr(opt_req_ip4_addr=dhcp_req_ip4_addr)) + self.dhcp_options.append(OptReqIpAddr(opt_req_ip4_addr=dhcp_req_ip4_addr)) if dhcp_addr_lease_time: - self.dhcp_options.append(DhcpOptAddrLeaseTime(opt_addr_lease_time=dhcp_addr_lease_time)) + self.dhcp_options.append(OptAddrLeaseTime(opt_addr_lease_time=dhcp_addr_lease_time)) if dhcp_srv_id: - self.dhcp_options.append(DhcpOptSrvId(opt_srv_id=dhcp_srv_id)) + self.dhcp_options.append(OptSrvId(opt_srv_id=dhcp_srv_id)) if dhcp_param_req_list: - self.dhcp_options.append(DhcpOptParamReqList(opt_param_req_list=dhcp_param_req_list)) + self.dhcp_options.append(OptParamReqList(opt_param_req_list=dhcp_param_req_list)) if dhcp_msg_type: - self.dhcp_options.append(DhcpOptMsgType(opt_msg_type=dhcp_msg_type)) + self.dhcp_options.append(OptMsgType(opt_msg_type=dhcp_msg_type)) - self.dhcp_options.append(DhcpOptEnd()) + self.dhcp_options.append(OptEnd()) def __str__(self): """Packet log string""" @@ -318,7 +306,7 @@ def dhcp_subnet_mask(self): """DHCP option - Subnet Mask (1)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_SUBNET_MASK: + if option.opt_code == OPT_SUBNET_MASK: return option.opt_subnet_mask return None @@ -327,7 +315,7 @@ def dhcp_router(self): """DHCP option - Router (3)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_ROUTER: + if option.opt_code == OPT_ROUTER: return option.opt_router return None @@ -336,7 +324,7 @@ def dhcp_dns(self): """DHCP option - Domain Name Server (6)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_DNS: + if option.opt_code == OPT_DNS: return option.opt_dns return None @@ -345,7 +333,7 @@ def dhcp_host_name(self): """DHCP option - Host Name (12)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_HOST_NAME: + if option.opt_code == OPT_HOST_NAME: return option.opt_host_name return None @@ -354,7 +342,7 @@ def dhcp_domain_name(self): """DHCP option - Domain Name (12)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_DOMAIN_NAME: + if option.opt_code == OPT_DOMAIN_NAME: return option.opt_domain_name return None @@ -363,7 +351,7 @@ def dhcp_req_ip4_addr(self): """DHCP option - Requested IP Address (50)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_REQ_IP4_ADDR: + if option.opt_code == OPT_REQ_IP4_ADDR: return option.opt_req_ip4_addr return None @@ -372,7 +360,7 @@ def dhcp_addr_lease_time(self): """DHCP option - Address Lease Time (51)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_ADDR_LEASE_TIME: + if option.opt_code == OPT_ADDR_LEASE_TIME: return option.opt_addr_lease_time return None @@ -381,7 +369,7 @@ def dhcp_msg_type(self): """DHCP option - Message Type (53)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_MSG_TYPE: + if option.opt_code == OPT_MSG_TYPE: return option.opt_msg_type return None @@ -390,7 +378,7 @@ def dhcp_srv_id(self): """DHCP option - Server Identivier (54)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_SRV_ID: + if option.opt_code == OPT_SRV_ID: return option.opt_srv_id return None @@ -399,7 +387,7 @@ def dhcp_param_req_list(self): """DHCP option - Parameter Request List (55)""" for option in self.dhcp_options: - if option.opt_code == DHCP_OPT_PARAM_REQ_LIST: + if option.opt_code == OPT_PARAM_REQ_LIST: return option.opt_param_req_list return None @@ -422,15 +410,15 @@ def get_raw_packet(self): # DHCP option - End (255) -DHCP_OPT_END = 255 -DHCP_OPT_END_LEN = 0 +OPT_END = 255 +OPT_END_LEN = 0 -class DhcpOptEnd: +class OptEnd: """DHCP option - End (255)""" def __init__(self): - self.opt_code = DHCP_OPT_END + self.opt_code = OPT_END @property def raw_option(self): @@ -442,15 +430,15 @@ def __str__(self): # DHCP option - Pad (0) -DHCP_OPT_PAD = 0 -DHCP_OPT_PAD_LEN = 0 +OPT_PAD = 0 +OPT_PAD_LEN = 0 -class DhcpOptPad: +class OptPad: """DHCP option - Pad (0)""" def __init__(self): - self.opt_code = DHCP_OPT_PAD + self.opt_code = OPT_PAD @property def raw_option(self): @@ -462,11 +450,11 @@ def __str__(self): # DHCP option - Subnet Mask (1) -DHCP_OPT_SUBNET_MASK = 1 -DHCP_OPT_SUBNET_MASK_LEN = 4 +OPT_SUBNET_MASK = 1 +OPT_SUBNET_MASK_LEN = 4 -class DhcpOptSubnetMask: +class OptSubnetMask: """DHCP option - Subnet Mask (1)""" def __init__(self, raw_option=None, opt_subnet_mask=None): @@ -475,8 +463,8 @@ def __init__(self, raw_option=None, opt_subnet_mask=None): self.opt_len = raw_option[1] self.opt_subnet_mask = IPv4Address(raw_option[2:6]) else: - self.opt_code = DHCP_OPT_SUBNET_MASK - self.opt_len = DHCP_OPT_SUBNET_MASK_LEN + self.opt_code = OPT_SUBNET_MASK + self.opt_len = OPT_SUBNET_MASK_LEN self.opt_subnet_mask = IPv4Address(opt_subnet_mask) @property @@ -489,11 +477,11 @@ def __str__(self): # DHCP option - Router (3) -DHCP_OPT_ROUTER = 3 -DHCP_OPT_ROUTER_LEN = None +OPT_ROUTER = 3 +OPT_ROUTER_LEN = None -class DhcpOptRouter: +class OptRouter: """DHCP option - Router (3)""" def __init__(self, raw_option=None, opt_router=None): @@ -502,7 +490,7 @@ def __init__(self, raw_option=None, opt_router=None): self.opt_len = raw_option[1] self.opt_router = [IPv4Address(raw_option[_ : _ + 4]) for _ in range(2, 2 + self.opt_len, 4)] else: - self.opt_code = DHCP_OPT_ROUTER + self.opt_code = OPT_ROUTER self.opt_len = len(opt_router) * 4 self.opt_router = [IPv4Address(_) for _ in opt_router] @@ -516,11 +504,11 @@ def __str__(self): # DHCP option - Domain Name Server (6) -DHCP_OPT_DNS = 6 -DHCP_OPT_DNS_LEN = None +OPT_DNS = 6 +OPT_DNS_LEN = None -class DhcpOptDns: +class OptDns: """DHCP option - Domain Name Server (6)""" def __init__(self, raw_option=None, opt_dns=None): @@ -529,7 +517,7 @@ def __init__(self, raw_option=None, opt_dns=None): self.opt_len = raw_option[1] self.opt_dns = [IPv4Address(raw_option[_ : _ + 4]) for _ in range(2, 2 + self.opt_len, 4)] else: - self.opt_code = DHCP_OPT_DNS + self.opt_code = OPT_DNS self.opt_len = len(opt_dns) * 4 self.opt_dns = [IPv4Address(_) for _ in opt_dns] @@ -543,11 +531,11 @@ def __str__(self): # DHCP option - Host Name (12) -DHCP_OPT_HOST_NAME = 12 -DHCP_OPT_HOST_NAME_LEN = None +OPT_HOST_NAME = 12 +OPT_HOST_NAME_LEN = None -class DhcpOptHostName: +class OptHostName: """DHCP option - Host Name (12)""" def __init__(self, raw_option=None, opt_host_name=None): @@ -556,7 +544,7 @@ def __init__(self, raw_option=None, opt_host_name=None): self.opt_len = raw_option[1] self.opt_host_name = str(raw_option[2 : 2 + self.opt_len], "utf-8") else: - self.opt_code = DHCP_OPT_HOST_NAME + self.opt_code = OPT_HOST_NAME self.opt_len = len(opt_host_name) self.opt_host_name = opt_host_name @@ -570,11 +558,11 @@ def __str__(self): # DHCP option - Domain Name (15) -DHCP_OPT_DOMAIN_NAME = 15 -DHCP_OPT_DOMAIN_NAME_LEN = None +OPT_DOMAIN_NAME = 15 +OPT_DOMAIN_NAME_LEN = None -class DhcpOptDomainName: +class OptDomainName: """DHCP option - Domain Name (15)""" def __init__(self, raw_option=None, opt_domain_name=None): @@ -583,7 +571,7 @@ def __init__(self, raw_option=None, opt_domain_name=None): self.opt_len = raw_option[1] self.opt_domain_name = str(raw_option[2 : 2 + self.opt_len], "utf-8") else: - self.opt_code = DHCP_OPT_DOMAIN_NAME + self.opt_code = OPT_DOMAIN_NAME self.opt_len = len(opt_domain_name) self.opt_domain_name = opt_domain_name @@ -597,11 +585,11 @@ def __str__(self): # DHCP option - Requested IP Address (50) -DHCP_OPT_REQ_IP4_ADDR = 50 -DHCP_OPT_REQ_IP4_ADDR_LEN = 4 +OPT_REQ_IP4_ADDR = 50 +OPT_REQ_IP4_ADDR_LEN = 4 -class DhcpOptReqIpAddr: +class OptReqIpAddr: """DHCP option - Requested IP Address (50)""" def __init__(self, raw_option=None, opt_req_ip4_addr=None): @@ -610,8 +598,8 @@ def __init__(self, raw_option=None, opt_req_ip4_addr=None): self.opt_len = raw_option[1] self.opt_req_ip4_addr = IPv4Address(raw_option[2:6]) else: - self.opt_code = DHCP_OPT_REQ_IP4_ADDR - self.opt_len = DHCP_OPT_REQ_IP4_ADDR_LEN + self.opt_code = OPT_REQ_IP4_ADDR + self.opt_len = OPT_REQ_IP4_ADDR_LEN self.opt_req_ip4_addr = IPv4Address(opt_req_ip4_addr) @property @@ -624,11 +612,11 @@ def __str__(self): # DHCP option - Address Lease Time (51) -DHCP_OPT_ADDR_LEASE_TIME = 51 -DHCP_OPT_ADDR_LEASE_TIME_LEN = 4 +OPT_ADDR_LEASE_TIME = 51 +OPT_ADDR_LEASE_TIME_LEN = 4 -class DhcpOptAddrLeaseTime: +class OptAddrLeaseTime: """DHCP option - Address Lease Time (51)""" def __init__(self, raw_option=None, opt_addr_lease_time=None): @@ -637,8 +625,8 @@ def __init__(self, raw_option=None, opt_addr_lease_time=None): self.opt_len = raw_option[1] self.opt_addr_lease_time = struct.unpack("!L", raw_option[2:6])[0] else: - self.opt_code = DHCP_OPT_ADDR_LEASE_TIME - self.opt_len = DHCP_OPT_ADDR_LEASE_TIME_LEN + self.opt_code = OPT_ADDR_LEASE_TIME + self.opt_len = OPT_ADDR_LEASE_TIME_LEN self.opt_addr_lease_time = opt_addr_lease_time @property @@ -651,11 +639,11 @@ def __str__(self): # DHCP option - Message Type (53) -DHCP_OPT_MSG_TYPE = 53 -DHCP_OPT_MSG_TYPE_LEN = 1 +OPT_MSG_TYPE = 53 +OPT_MSG_TYPE_LEN = 1 -class DhcpOptMsgType: +class OptMsgType: """DHCP option - Message Type (53)""" def __init__(self, raw_option=None, opt_msg_type=None): @@ -664,8 +652,8 @@ def __init__(self, raw_option=None, opt_msg_type=None): self.opt_len = raw_option[1] self.opt_msg_type = raw_option[2] else: - self.opt_code = DHCP_OPT_MSG_TYPE - self.opt_len = DHCP_OPT_MSG_TYPE_LEN + self.opt_code = OPT_MSG_TYPE + self.opt_len = OPT_MSG_TYPE_LEN self.opt_msg_type = opt_msg_type @property @@ -678,11 +666,11 @@ def __str__(self): # DHCP option - Server Identifier (54) -DHCP_OPT_SRV_ID = 54 -DHCP_OPT_SRV_ID_LEN = 4 +OPT_SRV_ID = 54 +OPT_SRV_ID_LEN = 4 -class DhcpOptSrvId: +class OptSrvId: """DHCP option - Server Identifier (54)""" def __init__(self, raw_option=None, opt_srv_id=None): @@ -691,8 +679,8 @@ def __init__(self, raw_option=None, opt_srv_id=None): self.opt_len = raw_option[1] self.opt_srv_id = IPv4Address(raw_option[2:6]) else: - self.opt_code = DHCP_OPT_SRV_ID - self.opt_len = DHCP_OPT_SRV_ID_LEN + self.opt_code = OPT_SRV_ID + self.opt_len = OPT_SRV_ID_LEN self.opt_srv_id = IPv4Address(opt_srv_id) @property @@ -705,11 +693,11 @@ def __str__(self): # DHCP option - Parameter Request List (55) -DHCP_OPT_PARAM_REQ_LIST = 55 -DHCP_OPT_PARAM_REQ_LIST_LEN = None +OPT_PARAM_REQ_LIST = 55 +OPT_PARAM_REQ_LIST_LEN = None -class DhcpOptParamReqList: +class OptParamReqList: """DHCP option - Parameter Request List (55)""" def __init__(self, raw_option=None, opt_param_req_list=None): @@ -718,7 +706,7 @@ def __init__(self, raw_option=None, opt_param_req_list=None): self.opt_len = raw_option[1] self.opt_param_req_list = raw_option[2 : 2 + self.opt_len] else: - self.opt_code = DHCP_OPT_PARAM_REQ_LIST + self.opt_code = OPT_PARAM_REQ_LIST self.opt_len = len(opt_param_req_list) self.opt_param_req_list = opt_param_req_list @@ -733,7 +721,7 @@ def __str__(self): # DHCP option not supported by this stack -class DhcpOptUnk: +class OptUnk: """DHCP option not supported by this stack""" def __init__(self, raw_option=None): diff --git a/ether/__init__.py b/ether/__init__.py new file mode 100755 index 00000000..c871ca5d --- /dev/null +++ b/ether/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ether/__init__.py +# diff --git a/ether/fpa.py b/ether/fpa.py new file mode 100755 index 00000000..18bf1967 --- /dev/null +++ b/ether/fpa.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ether/fpa.py - Fast Packet Assembler support class for Ethernet protocol +# + + +import struct +from typing import Union + +import arp.fpa +import ether.ps +import ip4.fpa +import ip6.fpa + + +class Assembler: + """Ethernet packet assembler support class""" + + def __init__( + self, carried_packet: Union[arp.fpa.Assembler, ip4.fpa.Assembler, ip6.fpa.Assembler], src: str = "00:00:00:00:00:00", dst: str = "00:00:00:00:00:00" + ) -> None: + """Class constructor""" + + assert carried_packet.ether_type in {ether.ps.TYPE_ARP, ether.ps.TYPE_IP4, ether.ps.TYPE_IP6} + + self._carried_packet = carried_packet + self.tracker = self._carried_packet.tracker + self.dst = dst + self.src = src + self.type = self._carried_packet.ether_type + + def __len__(self) -> int: + """Length of the packet""" + + return ether.ps.HEADER_LEN + len(self._carried_packet) + + from ether.ps import __str__ + + def assemble(self, frame: bytearray, hptr: int) -> None: + """Assemble packet into the raw form""" + + struct.pack_into("! 6s 6s H", frame, hptr, bytes.fromhex(self.dst.replace(":", "")), bytes.fromhex(self.src.replace(":", "")), self.type) + + self._carried_packet.assemble(frame, hptr + ether.ps.HEADER_LEN) diff --git a/ether/fpp.py b/ether/fpp.py new file mode 100755 index 00000000..53eb06dc --- /dev/null +++ b/ether/fpp.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ether/fpp.py - Fast Packet Parser support class for Ethernet protocol +# + + +import struct + +import config +import ether.ps +from misc.packet import PacketRx + + +class Parser: + """Ethernet packet parser class""" + + def __init__(self, packet_rx: PacketRx) -> None: + """Class constructor""" + + packet_rx.ether = self + + self._frame = packet_rx.frame + self._hptr = packet_rx.hptr + + packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() + + if not packet_rx.parse_failed: + packet_rx.hptr = self._hptr + ether.ps.HEADER_LEN + + def __len__(self) -> int: + """Number of bytes remaining in the frame""" + + return len(self._frame) - self._hptr + + from ether.ps import __str__ + + @property + def dst(self) -> str: + """Read 'Destination MAC address' field""" + + if "_cache__dst" not in self.__dict__: + self._cache__dst = ":".join([f"{_:0>2x}" for _ in self._frame[self._hptr + 0 : self._hptr + 6]]) + return self._cache__dst + + @property + def src(self) -> str: + """Read 'Source MAC address' field""" + + if "_cache__src" not in self.__dict__: + self._cache__src = ":".join([f"{_:0>2x}" for _ in self._frame[self._hptr + 6 : self._hptr + 12]]) + return self._cache__src + + @property + def type(self) -> int: + """Read 'EtherType' field""" + + if "_cache__type" not in self.__dict__: + self._cache__type = struct.unpack_from("!H", self._frame, self._hptr + 12)[0] + return self._cache__type + + @property + def header_copy(self) -> bytes: + """Return copy of packet header""" + + if "_cache__header_copy" not in self.__dict__: + self._cache__header_copy = self._frame[self._hptr : self._hptr + ether.ps.HEADER_LEN] + return self._cache__header_copy + + @property + def data_copy(self) -> bytes: + """Return copy of packet data""" + + if "_cache__data_copy" not in self.__dict__: + self._cache__data_copy = self._frame[self._hptr + ether.ps.HEADER_LEN :] + return self._cache__data_copy + + @property + def packet_copy(self) -> bytes: + """Return copy of whole packet""" + + if "_cache__packet_copy" not in self.__dict__: + self._cache__packet_copy = self._frame[self._hptr :] + return self._cache__packet_copy + + @property + def plen(self) -> int: + """Calculate packet length""" + + if "_cache__plen" not in self.__dict__: + self._cache__plen = len(self) + return self._cache__plen + + def _packet_integrity_check(self) -> str: + """Packet integrity check to be run on raw packet prior to parsing to make sure parsing is safe""" + + if not config.packet_integrity_check: + return "" + + if len(self) < ether.ps.HEADER_LEN: + return "ETHER integrity - wrong packet length (I)" + + return "" + + def _packet_sanity_check(self) -> str: + """Packet sanity check to be run on parsed packet to make sure packet's fields contain sane values""" + + if not config.packet_sanity_check: + return "" + + if self.type < ether.ps.TYPE_MIN: + return "ETHER sanity - 'ether_type' must be greater than 0x0600" + + return "" diff --git a/phrx_ether.py b/ether/phrx.py similarity index 63% rename from phrx_ether.py rename to ether/phrx.py index 96b6866c..9d2212b5 100755 --- a/phrx_ether.py +++ b/ether/phrx.py @@ -23,32 +23,25 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phrx_ether.py - packet handler for inbound Ethernet packets +# ether/phrx.py - packet handler for inbound Ethernet packets # +from typing import cast + import config -import fpp_ether +import ether.fpp +import ether.ps +from ether.fpp import Parser as EtherParser +from misc.packet import PacketRx -def _phrx_ether(self, packet_rx): +def _phrx_ether(self, packet_rx: PacketRx) -> None: """Handle inbound Ethernet packets""" - fpp_ether.EtherPacket(packet_rx) + ether.fpp.Parser(packet_rx) if packet_rx.parse_failed: if __debug__: @@ -58,20 +51,22 @@ def _phrx_ether(self, packet_rx): if __debug__: self._logger.debug(f"{packet_rx.tracker} - {packet_rx.ether}") + packet_rx.ether = cast(EtherParser, packet_rx.ether) + # Check if received packet matches any of stack MAC addresses if packet_rx.ether.dst not in {self.mac_unicast, *self.mac_multicast, self.mac_broadcast}: if __debug__: self._logger.opt(ansi=True).debug(f"{packet_rx.tracker} - Ethernet packet not destined for this stack, dropping...") return - if packet_rx.ether.type == fpp_ether.ETHER_TYPE_ARP and config.ip4_support: + if packet_rx.ether.type == ether.ps.TYPE_ARP and config.ip4_support: self._phrx_arp(packet_rx) return - if packet_rx.ether.type == fpp_ether.ETHER_TYPE_IP4 and config.ip4_support: + if packet_rx.ether.type == ether.ps.TYPE_IP4 and config.ip4_support: self._phrx_ip4(packet_rx) return - if packet_rx.ether.type == fpp_ether.ETHER_TYPE_IP6 and config.ip6_support: + if packet_rx.ether.type == ether.ps.TYPE_IP6 and config.ip6_support: self._phrx_ip6(packet_rx) return diff --git a/phtx_ether.py b/ether/phtx.py similarity index 80% rename from phtx_ether.py rename to ether/phtx.py index d7208737..5498f1d0 100755 --- a/phtx_ether.py +++ b/ether/phtx.py @@ -23,36 +23,31 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phtx_ether.py - packet handler for outbound Ethernet packets +# ether/phtx.py - packet handler for outbound Ethernet packets # +from typing import Union, cast -import fpa_ether +import arp.fpa +import ether.fpa +import ether.ps +import ip4.fpa +import ip6.fpa -def _phtx_ether(self, child_packet, ether_src="00:00:00:00:00:00", ether_dst="00:00:00:00:00:00"): +def _phtx_ether( + self, carried_packet: Union[arp.fpa.Assembler, ip4.fpa.Assembler, ip6.fpa.Assembler], ether_src="00:00:00:00:00:00", ether_dst="00:00:00:00:00:00" +) -> None: """Handle outbound Ethernet packets""" - def _send_out_packet(): + def _send_out_packet() -> None: if __debug__: self._logger.opt(depth=1).debug(f"{ether_packet_tx.tracker} - {ether_packet_tx}") self.tx_ring.enqueue(ether_packet_tx) - ether_packet_tx = fpa_ether.EtherPacket(src=ether_src, dst=ether_dst, child_packet=child_packet) + ether_packet_tx = ether.fpa.Assembler(src=ether_src, dst=ether_dst, carried_packet=carried_packet) # Check if packet contains valid source address, fill it out if needed if ether_packet_tx.src == "00:00:00:00:00:00": @@ -68,9 +63,10 @@ def _send_out_packet(): return # Check if we can obtain destination MAC based on IPv6 header data - if ether_packet_tx.type == fpa_ether.ETHER_TYPE_IP6: - ip6_src = ether_packet_tx._child_packet.src - ip6_dst = ether_packet_tx._child_packet.dst + if ether_packet_tx.type == ether.ps.TYPE_IP6: + ether_packet_tx._carried_packet = cast(ip6.fpa.Assembler, ether_packet_tx._carried_packet) + ip6_src = ether_packet_tx._carried_packet.src + ip6_dst = ether_packet_tx._carried_packet.dst # Send packet out if its destined to multicast IPv6 address if ip6_dst.is_multicast: @@ -105,9 +101,10 @@ def _send_out_packet(): return # Check if we can obtain destination MAC based on IPv4 header data - if ether_packet_tx.type == fpa_ether.ETHER_TYPE_IP4: - ip4_src = ether_packet_tx._child_packet.src - ip4_dst = ether_packet_tx._child_packet.dst + if ether_packet_tx.type == ether.ps.TYPE_IP4: + ether_packet_tx._carried_packet = cast(ip4.fpa.Assembler, ether_packet_tx._carried_packet) + ip4_src = ether_packet_tx._carried_packet.src + ip4_dst = ether_packet_tx._carried_packet.dst # Send out packet if its destinied to limited broadcast addresses if ip4_dst.is_limited_broadcast: diff --git a/ether/ps.py b/ether/ps.py new file mode 100755 index 00000000..2b1be0ef --- /dev/null +++ b/ether/ps.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ether/ps.py - protocol support for Ethernet +# + + +# Ethernet packet header + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | > +# + Destination MAC Address +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# > | > +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Source MAC Address + +# > | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | EtherType | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +HEADER_LEN = 14 + +TYPE_MIN = 0x0600 +TYPE_ARP = 0x0806 +TYPE_IP4 = 0x0800 +TYPE_IP6 = 0x86DD + +TYPE_TABLE = {TYPE_ARP: "ARP", TYPE_IP4: "IPv4", TYPE_IP6: "IPv6"} + + +def __str__(self) -> str: + """Packet log string""" + + return f"ETHER {self.src} > {self.dst}, 0x{self.type:0>4x} ({TYPE_TABLE.get(self.type, '???')})" diff --git a/fpa_arp.py b/fpa_arp.py deleted file mode 100755 index c8dc054d..00000000 --- a/fpa_arp.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpa_arp.py - Fast Packet Assembler support class for ARP protocol -# - - -import struct - -from ipv4_address import IPv4Address -from tracker import Tracker - -# ARP packet header - IPv4 stack version only - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Hardware Type | Protocol Type | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Hard Length | Proto Length | Operation | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + Sender Mac Address +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# > | Sender IP Address > -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# > | > -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Target MAC Address | -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Target IP Address | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -ARP_HEADER_LEN = 28 - -ARP_OP_REQUEST = 1 -ARP_OP_REPLY = 2 - - -class ArpPacket: - """ARP packet support class""" - - protocol = "ARP" - - def __init__(self, sha, spa, tpa, tha="00:00:00:00:00:00", oper=ARP_OP_REQUEST, echo_tracker=None): - """Class constructor""" - - self.tracker = Tracker("TX", echo_tracker) - - self.hrtype = 1 - self.prtype = 0x0800 - self.hrlen = 6 - self.prlen = 4 - self.oper = oper - self.sha = sha - self.spa = IPv4Address(spa) - self.tha = tha - self.tpa = IPv4Address(tpa) - - def __str__(self): - """Packet log string""" - - if self.oper == ARP_OP_REQUEST: - return f"ARP request {self.spa} / {self.sha} > {self.tpa} / {self.tha}" - if self.oper == ARP_OP_REPLY: - return f"ARP reply {self.spa} / {self.sha} > {self.tpa} / {self.tha}" - return f"ARP unknown operation {self.oper}" - - def __len__(self): - """Length of the packet""" - - return ARP_HEADER_LEN - - def assemble_packet(self, frame, hptr): - """Assemble packet into the raw form""" - - return struct.pack_into( - "!HH BBH 6s 4s 6s 4s", - frame, - hptr, - self.hrtype, - self.prtype, - self.hrlen, - self.prlen, - self.oper, - bytes.fromhex(self.sha.replace(":", "")), - IPv4Address(self.spa).packed, - bytes.fromhex(self.tha.replace(":", "")), - IPv4Address(self.tpa).packed, - ) diff --git a/fpa_ether.py b/fpa_ether.py deleted file mode 100755 index 8858b69e..00000000 --- a/fpa_ether.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpa_ether.py - Fast Packet Assembler support class for Ethernet protocol -# - - -import struct - -# Ethernet packet header - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + Destination MAC Address +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# > | > -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Source MAC Address + -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | EtherType | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -ETHER_HEADER_LEN = 14 - -ETHER_TYPE_MIN = 0x0600 -ETHER_TYPE_ARP = 0x0806 -ETHER_TYPE_IP4 = 0x0800 -ETHER_TYPE_IP6 = 0x86DD - - -ETHER_TYPE_TABLE = {ETHER_TYPE_ARP: "ARP", ETHER_TYPE_IP4: "IPv4", ETHER_TYPE_IP6: "IPv6"} - - -class EtherPacket: - """Ethernet packet support class""" - - protocol = "ETHER" - - def __init__(self, child_packet, src="00:00:00:00:00:00", dst="00:00:00:00:00:00"): - """Class constructor""" - - assert child_packet.protocol in {"IP6", "IP4", "ARP"}, f"Not supported protocol: {child_packet.protocol}" - self._child_packet = child_packet - - self.tracker = self._child_packet.tracker - - self.dst = dst - self.src = src - - if self._child_packet.protocol == "IP6": - self.type = ETHER_TYPE_IP6 - - if self._child_packet.protocol == "IP4": - self.type = ETHER_TYPE_IP4 - - if self._child_packet.protocol == "ARP": - self.type = ETHER_TYPE_ARP - - def __str__(self): - """Packet log string""" - - return f"ETHER {self.src} > {self.dst}, 0x{self.type:0>4x} ({ETHER_TYPE_TABLE.get(self.type, '???')})" - - def __len__(self): - """Length of the packet""" - - return ETHER_HEADER_LEN + len(self._child_packet) - - def assemble_packet(self, frame, hptr): - """Assemble packet into the raw form""" - - struct.pack_into("! 6s 6s H", frame, hptr, bytes.fromhex(self.dst.replace(":", "")), bytes.fromhex(self.src.replace(":", "")), self.type) - - self._child_packet.assemble_packet(frame, hptr + ETHER_HEADER_LEN) diff --git a/fpa_icmp4.py b/fpa_icmp4.py deleted file mode 100755 index 7087143f..00000000 --- a/fpa_icmp4.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpa_icmp4.py - Fast Packet Assembler support class for ICMPv4 protocol -# - - -import struct - -from ip_helper import inet_cksum -from tracker import Tracker - -# Echo reply message (0/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | Seq | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Destination Unreachable message (3/[0-3, 5-15]) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reserved | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Destination Unreachable message (3/4) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reserved | Link MTU / 0 | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Echo Request message (8/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | Seq | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -ICMP4_ECHO_REPLY = 0 -ICMP4_ECHO_REPLY_LEN = 8 -ICMP4_UNREACHABLE = 3 -ICMP4_UNREACHABLE_LEN = 8 -ICMP4_UNREACHABLE__NET = 0 -ICMP4_UNREACHABLE__HOST = 1 -ICMP4_UNREACHABLE__PROTOCOL = 2 -ICMP4_UNREACHABLE__PORT = 3 -ICMP4_UNREACHABLE__FAGMENTATION = 4 -ICMP4_UNREACHABLE__SOURCE_ROUTE_FAILED = 5 -ICMP4_ECHO_REQUEST = 8 -ICMP4_ECHO_REQUEST_LEN = 8 - - -class Icmp4Packet: - """ICMPv4 packet support class""" - - protocol = "ICMP4" - - def __init__( - self, - type, - code=0, - ec_id=None, - ec_seq=None, - ec_data=b"", - un_data=b"", - echo_tracker=None, - ): - """Class constructor""" - - self.tracker = Tracker("TX", echo_tracker) - - self.type = type - self.code = code - - if self.type == ICMP4_ECHO_REPLY: - self.ec_id = ec_id - self.ec_seq = ec_seq - self.ec_data = ec_data - - elif self.type == ICMP4_UNREACHABLE and self.code == ICMP4_UNREACHABLE__PORT: - self.un_data = un_data[:520] - - elif self.type == ICMP4_ECHO_REQUEST: - self.ec_id = ec_id - self.ec_seq = ec_seq - self.ec_data = ec_data - - def __str__(self): - """Packet log string""" - - log = f"ICMPv4 type {self.type}, code {self.code}" - - if self.type == ICMP4_ECHO_REPLY: - log += f", id {self.ec_id}, seq {self.ec_seq}" - - elif self.type == ICMP4_UNREACHABLE and self.code == ICMP4_UNREACHABLE__PORT: - pass - - elif self.type == ICMP4_ECHO_REQUEST: - log += f", id {self.ec_id}, seq {self.ec_seq}" - - return log - - def __len__(self): - """Length of the packet""" - - if self.type == ICMP4_ECHO_REPLY: - return ICMP4_ECHO_REPLY_LEN + len(self.ec_data) - - if self.type == ICMP4_UNREACHABLE and self.code == ICMP4_UNREACHABLE__PORT: - return ICMP4_UNREACHABLE_LEN + len(self.un_data) - - if self.type == ICMP4_ECHO_REQUEST: - return ICMP4_ECHO_REQUEST_LEN + len(self.ec_data) - - def assemble_packet(self, frame, hptr, _): - """Assemble packet into the raw form""" - - if self.type == ICMP4_ECHO_REPLY: - struct.pack_into(f"! BBH HH {len(self.ec_data)}s", frame, hptr, self.type, self.code, 0, self.ec_id, self.ec_seq, self.ec_data) - - elif self.type == ICMP4_UNREACHABLE and self.code == ICMP4_UNREACHABLE__PORT: - struct.pack_into(f"! BBH L {len(self.un_data)}s", frame, hptr, self.type, self.code, 0, 0, self.un_data) - - elif self.type == ICMP4_ECHO_REQUEST: - struct.pack_into(f"! BBH HH {len(self.ec_data)}s", frame, hptr, self.type, self.code, 0, self.ec_id, self.ec_seq, self.ec_data) - - struct.pack_into("! H", frame, hptr + 2, inet_cksum(frame, hptr, len(self))) diff --git a/fpa_ip6.py b/fpa_ip6.py deleted file mode 100755 index a6251ea3..00000000 --- a/fpa_ip6.py +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpa_ip6.py - Fast Packet Assembler support class for IPv6 protocol -# - - -import struct - -import config -from ipv6_address import IPv6Address - -# IPv6 protocol header - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# |Version| Traffic Class | Flow Label | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Payload Length | Next Header | Hop Limit | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + + -# > > -# + Source Address + -# > > -# + + -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + + -# > > -# + Destination Address + -# > > -# + + -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -IP6_HEADER_LEN = 40 - -IP6_NEXT_HEADER_TCP = 6 -IP6_NEXT_HEADER_UDP = 17 -IP6_NEXT_HEADER_EXT_FRAG = 44 -IP6_NEXT_HEADER_ICMP6 = 58 - -IP6_NEXT_HEADER_TABLE = {IP6_NEXT_HEADER_TCP: "TCP", IP6_NEXT_HEADER_UDP: "UDP", IP6_NEXT_HEADER_ICMP6: "ICMPv6", IP6_NEXT_HEADER_EXT_FRAG: "IPv6_FRAG"} - - -class Ip6Packet: - """IPv6 packet support class""" - - protocol = "IP6" - - def __init__( - self, - child_packet, - src, - dst, - hop=config.ip6_default_hop, - dscp=0, - ecn=0, - flow=0, - ): - """Class constructor""" - - assert child_packet.protocol in {"ICMP6", "UDP", "TCP", "IP6_EXT_FRAG"}, f"Not supported protocol: {child_packet.protocol}" - self._child_packet = child_packet - - self.tracker = self._child_packet.tracker - - self.ver = 6 - self.dscp = dscp - self.ecn = ecn - self.flow = flow - self.hop = hop - self.src = IPv6Address(src) - self.dst = IPv6Address(dst) - - if self._child_packet.protocol == "ICMP6": - self.next = IP6_NEXT_HEADER_ICMP6 - - elif self._child_packet.protocol == "UDP": - self.next = IP6_NEXT_HEADER_UDP - - elif self._child_packet.protocol == "TCP": - self.next = IP6_NEXT_HEADER_TCP - - elif self._child_packet.protocol == "IP6_EXT_FRAG": - self.next = IP6_NEXT_HEADER_EXT_FRAG - - self.dlen = len(child_packet) - - def __str__(self): - """Packet log string""" - - return ( - f"IPv6 {self.src} > {self.dst}, next {self.next} ({IP6_NEXT_HEADER_TABLE.get(self.next, '???')}), flow {self.flow}" - + f", dlen {self.dlen}, hop {self.hop}" - ) - - def __len__(self): - """Length of the packet""" - - return IP6_HEADER_LEN + len(self._child_packet) - - @property - def pshdr_sum(self): - """Returns IPv6 pseudo header that is used by TCP, UDP and ICMPv6 to compute their checksums""" - - pseudo_header = struct.pack("! 16s 16s L BBBB", self.src.packed, self.dst.packed, self.dlen, 0, 0, 0, self.next) - return sum(struct.unpack("! 5Q", pseudo_header)) - - def assemble_packet(self, frame, hptr): - """Assemble packet into the raw form""" - - struct.pack_into( - "! BBBB HBB 16s 16s", - frame, - hptr, - self.ver << 4 | self.dscp >> 4, - self.dscp << 6 | self.ecn << 4 | ((self.flow & 0b000011110000000000000000) >> 16), - (self.flow & 0b000000001111111100000000) >> 8, - self.flow & 0b000000000000000011111111, - self.dlen, - self.next, - self.hop, - self.src.packed, - self.dst.packed, - ) - - self._child_packet.assemble_packet(frame, hptr + IP6_HEADER_LEN, self.pshdr_sum) diff --git a/fpa_tcp.py b/fpa_tcp.py deleted file mode 100755 index aa101cc2..00000000 --- a/fpa_tcp.py +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpa_tcp.py - Fast Packet Assembler support class for TCP protocol -# - - -import struct - -from ip_helper import inet_cksum -from tracker import Tracker - -# TCP packet header (RFC 793) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Source Port | Destination Port | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Sequence Number | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Acknowledgment Number | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Hlen | Res |N|C|E|U|A|P|R|S|F| Window | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Checksum | Urgent Pointer | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Options ~ Padding ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -TCP_HEADER_LEN = 20 - - -class TcpPacket: - """TCP packet support class""" - - protocol = "TCP" - - def __init__( - self, - sport, - dport, - seq=0, - ack=0, - flag_ns=False, - flag_crw=False, - flag_ece=False, - flag_urg=False, - flag_ack=False, - flag_psh=False, - flag_rst=False, - flag_syn=False, - flag_fin=False, - win=0, - urp=0, - options=None, - data=b"", - echo_tracker=None, - ): - """Class constructor""" - - self.tracker = Tracker("TX", echo_tracker) - - self.sport = sport - self.dport = dport - self.seq = seq - self.ack = ack - self.flag_ns = flag_ns - self.flag_crw = flag_crw - self.flag_ece = flag_ece - self.flag_urg = flag_urg - self.flag_ack = flag_ack - self.flag_psh = flag_psh - self.flag_rst = flag_rst - self.flag_syn = flag_syn - self.flag_fin = flag_fin - self.win = win - self.urp = urp - - self.options = [] if options is None else options - - self.data = data - - self.hlen = TCP_HEADER_LEN + sum([len(_) for _ in self.options]) - - assert self.hlen % 4 == 0, f"TCP header len {self.hlen} is not multiplcation of 4 bytes, check options... {self.options}" - - def __str__(self): - """Packet log string""" - - log = ( - f"TCP {self.sport} > {self.dport}, {'N' if self.flag_ns else ''}{'C' if self.flag_crw else ''}" - + f"{'E' if self.flag_ece else ''}{'U' if self.flag_urg else ''}{'A' if self.flag_ack else ''}" - + f"{'P' if self.flag_psh else ''}{'R' if self.flag_rst else ''}{'S' if self.flag_syn else ''}" - + f"{'F' if self.flag_fin else ''}, seq {self.seq}, ack {self.ack}, win {self.win}, dlen {len(self.data)}" - ) - - for option in self.options: - log += ", " + str(option) - - return log - - def __len__(self): - """Length of the packet""" - - return self.hlen + len(self.data) - - @property - def raw_options(self): - """Packet options in raw format""" - - raw_options = b"" - - for option in self.options: - raw_options += option.raw_option - - return raw_options - - def assemble_packet(self, frame, hptr, pshdr_sum): - """Assemble packet into the raw form""" - - struct.pack_into( - f"! HH L L BBH HH {len(self.raw_options)}s {len(self.data)}s", - frame, - hptr, - self.sport, - self.dport, - self.seq, - self.ack, - self.hlen << 2 | self.flag_ns, - self.flag_crw << 7 - | self.flag_ece << 6 - | self.flag_urg << 5 - | self.flag_ack << 4 - | self.flag_psh << 3 - | self.flag_rst << 2 - | self.flag_syn << 1 - | self.flag_fin, - self.win, - 0, - self.urp, - self.raw_options, - self.data, - ) - - struct.pack_into("! H", frame, hptr + 16, inet_cksum(frame, hptr, len(self), pshdr_sum)) - - -# -# TCP options -# - - -# TCP option - End of Option List (0) - -TCP_OPT_EOL = 0 -TCP_OPT_EOL_LEN = 1 - - -class TcpOptEol: - """TCP option - End of Option List (0)""" - - @property - def raw_option(self): - return struct.pack("!B", TCP_OPT_EOL) - - def __str__(self): - return "eol" - - def __len__(self): - return TCP_OPT_EOL_LEN - - -# TCP option - No Operation (1) - -TCP_OPT_NOP = 1 -TCP_OPT_NOP_LEN = 1 - - -class TcpOptNop: - """TCP option - No Operation (1)""" - - @property - def raw_option(self): - return struct.pack("!B", TCP_OPT_NOP) - - def __str__(self): - return "nop" - - def __len__(self): - return TCP_OPT_NOP_LEN - - -# TCP option - Maximum Segment Size (2) - -TCP_OPT_MSS = 2 -TCP_OPT_MSS_LEN = 4 - - -class TcpOptMss: - """TCP option - Maximum Segment Size (2)""" - - def __init__(self, mss): - self.mss = mss - - @property - def raw_option(self): - return struct.pack("! BB H", TCP_OPT_MSS, TCP_OPT_MSS_LEN, self.mss) - - def __str__(self): - return f"mss {self.mss}" - - def __len__(self): - return TCP_OPT_MSS_LEN - - -# TCP option - Window Scale (3) - -TCP_OPT_WSCALE = 3 -TCP_OPT_WSCALE_LEN = 3 - - -class TcpOptWscale: - """TCP option - Window Scale (3)""" - - def __init__(self, wscale): - self.wscale = wscale - - @property - def raw_option(self): - return struct.pack("! BB B", TCP_OPT_WSCALE, TCP_OPT_WSCALE_LEN, self.wscale) - - def __str__(self): - return f"wscale {self.wscale}" - - def __len__(self): - return TCP_OPT_WSCALE_LEN - - -# TCP option - Sack Permit (4) - -TCP_OPT_SACKPERM = 4 -TCP_OPT_SACKPERM_LEN = 2 - - -class TcpOptSackPerm: - """TCP option - Sack Permit (4)""" - - @property - def raw_option(self): - return struct.pack("! BB", TCP_OPT_SACKPERM, TCP_OPT_SACKPERM_LEN) - - def __str__(self): - return "sack_perm" - - def __len__(self): - return TCP_OPT_SACKPERM_LEN - - -# TCP option - Timestamp - -TCP_OPT_TIMESTAMP = 8 -TCP_OPT_TIMESTAMP_LEN = 10 - - -class TcpOptTimestamp: - """TCP option - Timestamp (8)""" - - def __init__(self, tsval, tsecr): - self.tsval = tsval - self.tsecr = tsecr - - @property - def raw_option(self): - return struct.pack("! BB LL", TCP_OPT_TIMESTAMP, TCP_OPT_TIMESTAMP_LEN, self.tsval, self.tsecr) - - def __str__(self): - return f"ts {self.tsval}/{self.tsecr}" - - def __len__(self): - return TCP_OPT_TIMESTAMP_LEN diff --git a/fpp_arp.py b/fpp_arp.py deleted file mode 100755 index da4778df..00000000 --- a/fpp_arp.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpp_arp.py - Fast Packet Parser support class for ARP protocol -# - - -import struct - -import config -from ipv4_address import IPv4Address - -# ARP packet header - IPv4 stack version only - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Hardware type | Protocol type | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Hard length | Proto length | Operation | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + Sender MAC address +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# > | Sender IP address > -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# > | > -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Target MAC address | -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Target IP address | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -ARP_HEADER_LEN = 28 - -ARP_OP_REQUEST = 1 -ARP_OP_REPLY = 2 - - -class ArpPacket: - """ARP packet support class""" - - class __not_cached: - pass - - def __init__(self, packet_rx): - """Class constructor""" - - packet_rx.arp = self - - self._frame = packet_rx.frame - self._hptr = packet_rx.hptr - - self.__hrtype = self.__not_cached - self.__prtype = self.__not_cached - self.__hrlen = self.__not_cached - self.__prlen = self.__not_cached - self.__oper = self.__not_cached - self.__sha = self.__not_cached - self.__spa = self.__not_cached - self.__tha = self.__not_cached - self.__tpa = self.__not_cached - self.__packet_copy = self.__not_cached - - packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() - - def __str__(self): - """Packet log string""" - - if self.oper == ARP_OP_REQUEST: - return f"ARP request {self.spa} / {self.sha} > {self.tpa} / {self.tha}" - if self.oper == ARP_OP_REPLY: - return f"ARP reply {self.spa} / {self.sha} > {self.tpa} / {self.tha}" - - def __len__(self): - """Number of bytes remaining in the frame""" - - return len(self._frame) - self._hptr - - @property - def hrtype(self): - """Read 'Hardware address type' field""" - - if self.__hrtype is self.__not_cached: - self.__hrtype = struct.unpack_from("!H", self._frame, self._hptr + 0)[0] - return self.__hrtype - - @property - def prtype(self): - """Read 'Protocol address type' field""" - - if self.__prtype is self.__not_cached: - self.__prtype = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] - return self.__prtype - - @property - def hrlen(self): - """Read 'Hardware address length' field""" - - return self._frame[self._hptr + 4] - - @property - def prlen(self): - """Read 'Protocol address length' field""" - - return self._frame[self._hptr + 5] - - @property - def oper(self): - """Read 'Operation' field""" - - if self.__oper is self.__not_cached: - self.__oper = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] - return self.__oper - - @property - def sha(self): - """Read 'Sender hardware address' field""" - - if self.__sha is self.__not_cached: - self.__sha = ":".join([f"{_:0>2x}" for _ in self._frame[self._hptr + 8 : self._hptr + 14]]) - return self.__sha - - @property - def spa(self): - """Read 'Sender protocol address' field""" - - if self.__spa is self.__not_cached: - self.__spa = IPv4Address(self._frame[self._hptr + 14 : self._hptr + 18]) - return self.__spa - - @property - def tha(self): - """Read 'Target hardware address' field""" - - if self.__tha is self.__not_cached: - self.__tha = ":".join([f"{_:0>2x}" for _ in self._frame[self._hptr + 18 : self._hptr + 24]]) - return self.__tha - - @property - def tpa(self): - """Read 'Target protocol address' field""" - - if self.__tpa is self.__not_cached: - self.__tpa = IPv4Address(self._frame[self._hptr + 24 : self._hptr + 28]) - return self.__tpa - - @property - def packet_copy(self): - """Read the whole packet""" - - if self.__packet_copy is self.__not_cached: - self.__packet_copy = self._frame[self._hptr : self._hptr + ARP_HEADER_LEN] - return self.__packet_copy - - def _packet_integrity_check(self): - """Packet integrity check to be run on raw packet prior to parsing to make sure parsing is safe""" - - if not config.packet_integrity_check: - return False - - if len(self) < ARP_HEADER_LEN: - return "ARP integrity - wrong packet length (I)" - - return False - - def _packet_sanity_check(self): - """Packet sanity check to be run on parsed packet to make sure packet's fields contain sane values""" - - if not config.packet_sanity_check: - return False - - if self.hrtype != 1: - return "ARP sanity - 'arp_hrtype' must be 1" - - if self.prtype != 0x0800: - return "ARP sanity - 'arp_prtype' must be 0x0800" - - if self.hrlen != 6: - return "ARP sanity - 'arp_hrlen' must be 6" - - if self.prlen != 4: - return "ARP sanity - 'arp_prlen' must be 4" - - if self.oper not in {1, 2}: - return "ARP sanity - 'oper' must be [1-2]" - - return False diff --git a/fpp_ether.py b/fpp_ether.py deleted file mode 100755 index 12c32d77..00000000 --- a/fpp_ether.py +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpp_ether.py - Fast Packet Parser support class for Ethernet protocol -# - - -import struct - -import config - -# Ethernet packet header - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + Destination MAC Address +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# > | > -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Source MAC Address + -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | EtherType | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -ETHER_HEADER_LEN = 14 - -ETHER_TYPE_MIN = 0x0600 -ETHER_TYPE_ARP = 0x0806 -ETHER_TYPE_IP4 = 0x0800 -ETHER_TYPE_IP6 = 0x86DD - - -ETHER_TYPE_TABLE = {ETHER_TYPE_ARP: "ARP", ETHER_TYPE_IP4: "IPv4", ETHER_TYPE_IP6: "IPv6"} - - -class EtherPacket: - """Ethernet packet support class""" - - class __not_cached: - pass - - def __init__(self, packet_rx): - """Class constructor""" - - packet_rx.ether = self - - self._frame = packet_rx.frame - self._hptr = packet_rx.hptr - - self.__dst = self.__not_cached - self.__src = self.__not_cached - self.__type = self.__not_cached - self.__header_copy = self.__not_cached - self.__data_copy = self.__not_cached - self.__packet_copy = self.__not_cached - self.__plen = self.__not_cached - - packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() - - if not packet_rx.parse_failed: - packet_rx.hptr = self._hptr + ETHER_HEADER_LEN - - def __str__(self): - """Packet log string""" - - return f"ETHER {self.src} > {self.dst}, 0x{self.type:0>4x} ({ETHER_TYPE_TABLE.get(self.type, '???')})" - - def __len__(self): - """Number of bytes remaining in the frame""" - - return len(self._frame) - self._hptr - - @property - def dst(self): - """Read 'Destination MAC address' field""" - - if self.__dst is self.__not_cached: - self.__dst = ":".join([f"{_:0>2x}" for _ in self._frame[self._hptr + 0 : self._hptr + 6]]) - return self.__dst - - @property - def src(self): - """Read 'Source MAC address' field""" - - if self.__src is self.__not_cached: - self.__src = ":".join([f"{_:0>2x}" for _ in self._frame[self._hptr + 6 : self._hptr + 12]]) - return self.__src - - @property - def type(self): - """Read 'EtherType' field""" - - if self.__type is self.__not_cached: - self.__type = struct.unpack_from("!H", self._frame, self._hptr + 12)[0] - return self.__type - - @property - def header_copy(self): - """Return copy of packet header""" - - if self.__header_copy is self.__not_cached: - self.__header_copy = self._frame[self._hptr : self._hptr + ETHER_HEADER_LEN] - return self.__header_copy - - @property - def data_copy(self): - """Return copy of packet data""" - - if self.__data_copy is self.__not_cached: - self.__data_copy = self._frame[self._hptr + ETHER_HEADER_LEN :] - return self.__data_copy - - @property - def packet_copy(self): - """Return copy of whole packet""" - - if self.__packet_copy is self.__not_cached: - self.__packet_copy = self._frame[self._hptr :] - return self.__packet_copy - - @property - def plen(self): - """Calculate packet length""" - - if self.__plen is self.__not_cached: - self.__plen = len(self) - return self.__plen - - def _packet_integrity_check(self): - """Packet integrity check to be run on raw packet prior to parsing to make sure parsing is safe""" - - if not config.packet_integrity_check: - return False - - if len(self) < ETHER_HEADER_LEN: - return "ETHER integrity - wrong packet length (I)" - - return False - - def _packet_sanity_check(self): - """Packet sanity check to be run on parsed packet to make sure packet's fields contain sane values""" - - if not config.packet_sanity_check: - return False - - if self.type < ETHER_TYPE_MIN: - return "ETHER sanity - 'ether_type' must be greater than 0x0600" - - return False diff --git a/fpp_icmp4.py b/fpp_icmp4.py deleted file mode 100755 index de872c42..00000000 --- a/fpp_icmp4.py +++ /dev/null @@ -1,261 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpp_icmp4.py - Fast Packet Parser support class for ICMPv4 protocol -# - - -import struct - -import config -from ip_helper import inet_cksum - -# Echo reply message (0/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | Seq | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Destination Unreachable message (3/[0-3, 5-15]) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | Seq | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reserved | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Destination Unreachable message (3/4) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | Seq | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reserved | Link MTU / 0 | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Echo Request message (8/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | Seq | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -ICMP4_HEADER_LEN = 4 - -ICMP4_ECHO_REPLY = 0 -ICMP4_UNREACHABLE = 3 -ICMP4_UNREACHABLE__NET = 0 -ICMP4_UNREACHABLE__HOST = 1 -ICMP4_UNREACHABLE__PROTOCOL = 2 -ICMP4_UNREACHABLE__PORT = 3 -ICMP4_UNREACHABLE__FAGMENTATION = 4 -ICMP4_UNREACHABLE__SOURCE_ROUTE_FAILED = 5 -ICMP4_ECHO_REQUEST = 8 - - -class Icmp4Packet: - """ICMPv4 packet support class""" - - class __not_cached: - pass - - def __init__(self, packet_rx): - """Class constructor""" - - packet_rx.icmp4 = self - - self._frame = packet_rx.frame - self._hptr = packet_rx.hptr - self._plen = packet_rx.ip.dlen - - self.__cksum = self.__not_cached - self.__ec_id = self.__not_cached - self.__ec_seq = self.__not_cached - self.__ec_data = self.__not_cached - self.__un_data = self.__not_cached - self.__plen = self.__not_cached - self.__packet_copy = self.__not_cached - - packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() - - def __str__(self): - """Packet log string""" - - log = f"ICMPv4 type {self.type}, code {self.code}" - - if self.type == ICMP4_ECHO_REPLY: - log += f", id {self.ec_id}, seq {self.ec_seq}" - - elif self.type == ICMP4_UNREACHABLE and self.code == ICMP4_UNREACHABLE__PORT: - pass - - elif self.type == ICMP4_ECHO_REQUEST: - log += f", id {self.ec_id}, seq {self.ec_seq}" - - return log - - def __len__(self): - """Number of bytes remaining in the frame""" - - return len(self._frame) - self._hptr - - @property - def type(self): - """Read 'Type' field""" - - return self._frame[self._hptr + 0] - - @property - def code(self): - """Read 'Code' field""" - - return self._frame[self._hptr + 1] - - @property - def cksum(self): - """Read 'Checksum' field""" - - if self.__cksum is self.__not_cached: - self.__cksum = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] - return self.__cksum - - @property - def ec_id(self): - """Read Echo 'Id' field""" - - if self.__ec_id is self.__not_cached: - assert self.type in {ICMP4_ECHO_REQUEST, ICMP4_ECHO_REPLY} - self.__ec_id = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] - return self.__ec_id - - @property - def ec_seq(self): - """Read Echo 'Seq' field""" - - if self.__ec_seq is self.__not_cached: - assert self.type in {ICMP4_ECHO_REQUEST, ICMP4_ECHO_REPLY} - self.__ec_seq = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] - return self.__ec_seq - - @property - def ec_data(self): - """Read data carried by Echo message""" - - if self.__ec_data is self.__not_cached: - assert self.type in {ICMP4_ECHO_REQUEST, ICMP4_ECHO_REPLY} - self.__ec_data = self._frame[self._hptr + 8 : self._hptr + self.plen] - return self.__ec_data - - @property - def un_data(self): - """Read data carried by Uneachable message""" - - if self.__un_data is self.__not_cached: - assert self.type == ICMP4_UNREACHABLE - self.__un_data = self._frame[self._hptr + 8 : self._hptr + self.plen] - return self.__un_data - - @property - def plen(self): - """Calculate packet length""" - - return self._plen - - @property - def packet_copy(self): - """Read the whole packet""" - - if self.__packet_copy is self.__not_cached: - self.__packet_copy = self._frame[self._hptr : self._hptr + self.plen] - return self.__packet_copy - - def _packet_integrity_check(self): - """Packet integrity check to be run on raw frame prior to parsing to make sure parsing is safe""" - - if not config.packet_integrity_check: - return False - - if inet_cksum(self._frame, self._hptr, self._plen): - return "ICMPv4 integrity - wrong packet checksum" - - if not ICMP4_HEADER_LEN <= self._plen <= len(self): - return "ICMPv4 integrity - wrong packet length (I)" - - if self._frame[self._hptr + 0] in {ICMP4_ECHO_REQUEST, ICMP4_ECHO_REPLY}: - if not 8 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - - elif self._frame[self._hptr + 0] == ICMP4_UNREACHABLE: - if not 12 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - - return False - - def _packet_sanity_check(self): - """Packet sanity check to be run on parsed packet to make sure frame's fields contain sane values""" - - if not config.packet_sanity_check: - return False - - if self.type in {ICMP4_ECHO_REQUEST, ICMP4_ECHO_REPLY}: - if not self.code == 0: - return "ICMPv4 sanity - 'code' should be set to 0 (RFC 792)" - - if self.type == ICMP4_UNREACHABLE: - if self.code not in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}: - return "ICMPv4 sanity - 'code' must be set to [0-15] (RFC 792)" - - return False diff --git a/fpp_icmp6.py b/fpp_icmp6.py deleted file mode 100755 index 86e84880..00000000 --- a/fpp_icmp6.py +++ /dev/null @@ -1,998 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpp_icmp6.py - Fast Packet Parse support class for ICMPv6 protocol -# - - -import struct - -import config -from ip_helper import inet_cksum -from ipv6_address import IPv6Address, IPv6Network - -# Destination Unreachable message (1/[0-6]) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | Seq | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reserved | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Packet Too Big message (2/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | MTU | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Time Exceeded (3/[0-1]) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Unused | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -# Parameter Problem message (4/[0-2]) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Pointer | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -# Echo Request message (128/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | Seq | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Echo Reply message (129/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | Seq | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Data ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# MLDv2 - Multicast Listener Query message (130/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Maximum Response Code | Reserved | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | | -# + * -# | | -# + Multicast Address * -# | | -# + * -# | | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Resv |S| QRV | QQIC | Number of Sources (N) | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | | -# + + -# | | -# + Source Address [1] + -# | | -# + + -# | | -# +---------------------------------------------------------------+ -# | | -# + + -# | | -# + Source Address [2] + -# | | -# + + -# | | -# +---------------------------------------------------------------+ -# . . . -# . . . -# . . . -# +---------------------------------------------------------------+ -# | | -# + + -# | | -# + Source Address [N] + -# | | -# + + -# | | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -# Router Solicitation message (133/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reserved | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Options ... -# +-+-+-+-+-+-+-+-+-+-+-+- - - -# Router Advertisement message (134/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Hop Limit |M|O|H|PRF|P|0|0| Router Lifetime | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reachable Time | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Retrans Timer | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Options ... -# +-+-+-+-+-+-+-+-+-+-+-+- - - -# Neighbor Solicitation message (135/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reserved | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + + -# > > -# + Target Address + -# > > -# + + -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Options ... -# +-+-+-+-+-+-+-+-+-+-+-+- - - -# Neighbor Advertisement message (136/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# |R|S|O| Reserved | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + + -# > > -# + Target Address + -# > > -# + + -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Options ... -# +-+-+-+-+-+-+-+-+-+-+-+- - - -# MLDv2 - Multicast Listener Report message (143/0) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Code | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reserved |Nr of Mcast Address Records (M)| -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ ~ -# ~ Multicast Address Record [1] ~ -# ~ ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ ~ -# ~ Multicast Address Record [2] ~ -# ~ ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# . . . -# . . . -# . . . -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ ~ -# ~ Multicast Address Record [M] ~ -# ~ ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -# Each Multicast Address Record has the following internal format: - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Record Type | Aux Data Len | Number of Sources (N) | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | | -# + + -# | | -# + Multicast Address + -# | | -# + + -# | | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | | -# + + -# | | -# + Source Address [1] + -# | | -# + + -# | | -# +---------------------------------------------------------------+ -# | | -# + + -# | | -# + Source Address [2] + -# | | -# + + -# | | -# +---------------------------------------------------------------+ -# . . . -# . . . -# . . . -# +---------------------------------------------------------------+ -# | | -# + + -# | | -# + Source Address [N] + -# | | -# + + -# | | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ ~ -# ~ Auxiliary Data ~ -# ~ ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -ICMP6_HEADER_LEN = 4 - -ICMP6_UNREACHABLE = 1 -ICMP6_UNREACHABLE__NO_ROUTE = 0 -ICMP6_UNREACHABLE__PROHIBITED = 1 -ICMP6_UNREACHABLE__SCOPE = 2 -ICMP6_UNREACHABLE__ADDRESS = 3 -ICMP6_UNREACHABLE__PORT = 4 -ICMP6_UNREACHABLE__FAILED_POLICY = 5 -ICMP6_UNREACHABLE__REJECT_ROUTE = 6 -ICMP6_PACKET_TOO_BIG = 2 -ICMP6_TIME_EXCEEDED = 3 -ICMP6_PARAMETER_PROBLEM = 4 -ICMP6_ECHO_REQUEST = 128 -ICMP6_ECHO_REPLY = 129 -ICMP6_MLD2_QUERY = 130 -ICMP6_ROUTER_SOLICITATION = 133 -ICMP6_ROUTER_ADVERTISEMENT = 134 -ICMP6_NEIGHBOR_SOLICITATION = 135 -ICMP6_NEIGHBOR_ADVERTISEMENT = 136 -ICMP6_MLD2_REPORT = 143 - - -ICMP6_MART_MODE_IS_INCLUDE = 1 -ICMP6_MART_MODE_IS_EXCLUDE = 2 -ICMP6_MART_CHANGE_TO_INCLUDE = 3 -ICMP6_MART_CHANGE_TO_EXCLUDE = 4 -ICMP6_MART_ALLOW_NEW_SOURCES = 5 -ICMP6_MART_BLOCK_OLD_SOURCES = 6 - - -class Icmp6Packet: - """ICMPv6 packet support class""" - - class __not_cached: - pass - - def __init__(self, packet_rx): - """Class constructor""" - - packet_rx.icmp6 = self - - self._frame = packet_rx.frame - self._hptr = packet_rx.hptr - self._plen = packet_rx.ip.dlen - - self.__cksum = self.__not_cached - self.__un_data = self.__not_cached - self.__ec_id = self.__not_cached - self.__ec_seq = self.__not_cached - self.__ec_data = self.__not_cached - self.__ra_flag_m = self.__not_cached - self.__ra_flag_o = self.__not_cached - self.__ra_router_lifetime = self.__not_cached - self.__ra_reachable_time = self.__not_cached - self.__ra_retrans_timer = self.__not_cached - self.__ns_target_address = self.__not_cached - self.__na_flag_r = self.__not_cached - self.__na_flag_s = self.__not_cached - self.__na_flag_o = self.__not_cached - self.__na_target_address = self.__not_cached - self.__mld2_rep_nor = self.__not_cached - self.__mld2_rep_records = self.__not_cached - self.__nd_options = self.__not_cached - self.__nd_opt_slla = self.__not_cached - self.__nd_opt_tlla = self.__not_cached - self.__nd_opt_pi = self.__not_cached - self.__packet_copy = self.__not_cached - - packet_rx.parse_failed = self._packet_integrity_check(packet_rx.ip6.pshdr_sum) or self._packet_sanity_check( - packet_rx.ip6.src, packet_rx.ip6.dst, packet_rx.ip6.hop - ) - - def __str__(self): - """Packet log string""" - - log = f"ICMPv6 type {self.type}, code {self.code}" - - if self.type == ICMP6_UNREACHABLE: - pass - - elif self.type == ICMP6_ECHO_REQUEST: - log += f", id {self.ec_id}, seq {self.ec_seq}" - - elif self.type == ICMP6_ECHO_REPLY: - log += f", id {self.ec_id}, seq {self.ec_seq}" - - elif self.type == ICMP6_ROUTER_SOLICITATION: - for option in self.nd_options: - log += ", " + str(option) - - elif self.type == ICMP6_ROUTER_ADVERTISEMENT: - log += f", hop {self.ra_hop}" - log += f"flags {'M' if self.ra_flag_m else '-'}{'O' if self.ra_flag_o else '-'}" - log += f"rlft {self.ra_router_lifetime}, reacht {self.ra_reachable_time}, retrt {self.ra_retrans_timer}" - for option in self.nd_options: - log += ", " + str(option) - - elif self.type == ICMP6_NEIGHBOR_SOLICITATION: - log += f", target {self.ns_target_address}" - for option in self.nd_options: - log += ", " + str(option) - - elif self.type == ICMP6_NEIGHBOR_ADVERTISEMENT: - log += f", target {self.na_target_address}" - log += f", flags {'R' if self.na_flag_r else '-'}{'S' if self.na_flag_s else '-'}{'O' if self.na_flag_o else '-'}" - for option in self.nd_options: - log += ", " + str(option) - - elif self.type == ICMP6_MLD2_REPORT: - pass - - return log - - def __len__(self): - """Number of bytes remaining in the frame""" - - return len(self._frame) - self._hptr - - @property - def type(self): - """Read 'Type' field""" - - return self._frame[self._hptr + 0] - - @property - def code(self): - """Read 'Code' field""" - - return self._frame[self._hptr + 1] - - @property - def cksum(self): - """Read 'Checksum' field""" - - if self.__cksum is self.__not_cached: - self.__cksum = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] - return self.__cksum - - @property - def un_data(self): - """Read data carried by Unreachable message""" - - if self.__un_data is self.__not_cached: - assert self.type == ICMP6_UNREACHABLE - self.__un_data = self._frame[self._hptr + 8 : self._hptr + self.plen] - return self.__un_data - - @property - def ec_id(self): - """Read Echo 'Id' field""" - - if self.__ec_id is self.__not_cached: - assert self.type in {ICMP6_ECHO_REQUEST, ICMP6_ECHO_REPLY} - self.__ec_id = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] - return self.__ec_id - - @property - def ec_seq(self): - """Read Echo 'Seq' field""" - - if self.__ec_seq is self.__not_cached: - assert self.type in {ICMP6_ECHO_REQUEST, ICMP6_ECHO_REPLY} - self.__ec_seq = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] - return self.__ec_seq - - @property - def ec_data(self): - """Read data carried by Echo message""" - - if self.__ec_data is self.__not_cached: - assert self.type in {ICMP6_ECHO_REQUEST, ICMP6_ECHO_REPLY} - self.__ec_data = self._frame[self._hptr + 8 : self._hptr + self.plen] - return self.__ec_data - - @property - def ra_hop(self): - """Read ND RA 'Hop limit' field""" - - assert self.type == ICMP6_ROUTER_ADVERTISEMENT - return self._frame[self._hptr + 4] - - @property - def ra_flag_m(self): - """Read ND RA 'M flag' field""" - - if self.__ra_flag_m is self.__not_cached: - assert self.type == ICMP6_ROUTER_ADVERTISEMENT - self.__ra_flag_m = bool(self._frame[self._hptr + 5] & 0b10000000) - return self.__ra_flag_m - - @property - def ra_flag_o(self): - """Read ND RA 'O flag' field""" - - if self.__ra_flag_o is self.__not_cached: - assert self.type == ICMP6_ROUTER_ADVERTISEMENT - self.__ra_flag_o = bool(self._frame[self._hptr + 5] & 0b01000000) - return self.__ra_flag_o - - @property - def ra_router_lifetime(self): - """Read ND RA 'Router lifetime' field""" - - if self.__ra_router_lifetime is self.__not_cached: - assert self.type == ICMP6_ROUTER_ADVERTISEMENT - self.__ra_router_lifetime = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] - return self.__ra_router_lifetime - - @property - def ra_reachable_time(self): - """Read ND RA 'Reachable time' field""" - - if self.__ra_reachable_time is self.__not_cached: - assert self.type == ICMP6_ROUTER_ADVERTISEMENT - self.__ra_reachable_time = struct.unpack_from("!L", self._frame, self._hptr + 8)[0] - return self.__ra_reachable_time - - @property - def ra_retrans_timer(self): - """Read ND RA 'Retransmision timer' field""" - - if self.__ra_retrans_timer is self.__not_cached: - assert self.type == ICMP6_ROUTER_ADVERTISEMENT - self.__ra_retrans_timer = struct.unpack_from("!L", self._frame, self._hptr + 12)[0] - return self.__ra_retrans_timer - - @property - def ns_target_address(self): - """Read ND NS 'Target adress' field""" - - if self.__ns_target_address is self.__not_cached: - assert self.type == ICMP6_NEIGHBOR_SOLICITATION - self.__ns_target_address = IPv6Address(self._frame[self._hptr + 8 : self._hptr + 24]) - return self.__ns_target_address - - @property - def na_flag_r(self): - """Read ND NA 'R flag' field""" - - if self.__na_flag_r is self.__not_cached: - assert self.type == ICMP6_NEIGHBOR_ADVERTISEMENT - self.__na_flag_r = bool(self._frame[self._hptr + 4] & 0b10000000) - return self.__na_flag_r - - @property - def na_flag_s(self): - """Read ND NA 'S flag' field""" - - if self.__na_flag_s is self.__not_cached: - assert self.type == ICMP6_NEIGHBOR_ADVERTISEMENT - self.__na_flag_s = bool(self._frame[self._hptr + 4] & 0b01000000) - return self.__na_flag_s - - @property - def na_flag_o(self): - """Read ND NA 'O flag' field""" - - if self.__na_flag_o is self.__not_cached: - assert self.type == ICMP6_NEIGHBOR_ADVERTISEMENT - self.__na_flag_o = bool(self._frame[self._hptr + 4] & 0b00100000) - return self.__na_flag_o - - @property - def na_target_address(self): - """Read ND NA 'Taret address' field""" - - if self.__na_target_address is self.__not_cached: - assert self.type == ICMP6_NEIGHBOR_ADVERTISEMENT - self.__na_target_address = IPv6Address(self._frame[self._hptr + 8 : self._hptr + 24]) - return self.__na_target_address - - @property - def mld2_rep_nor(self): - """Read MLD2 Report 'Number of multicast address records' field""" - - if self.__mld2_rep_nor is self.__not_cached: - assert self.type == ICMP6_MLD2_REPORT - self.__mld2_rep_nor = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] - return self.__mld2_rep_nor - - @property - def mld2_rep_records(self): - """Read MLD2 Report record list""" - - if self.__mld2_rep_records is self.__not_cached: - assert self.type == ICMP6_MLD2_REPORT - self.__mld2_rep_records = [] - raw_records = self._frame[self._hptr + 8 :] - for _ in range(self.mld2_rep_nor): - record = MulticastAddressRecord(raw_records) - raw_records = raw_records[len(record) :] - self.__mld2_rep_records.append(record) - return self.__mld2_rep_records - - def _read_nd_options(self, optr): - """Read ND options""" - - nd_options = [] - while optr < len(self._frame): - nd_options.append( - {ICMP6_ND_OPT_SLLA: Icmp6NdOptSLLA, ICMP6_ND_OPT_TLLA: Icmp6NdOptTLLA, ICMP6_ND_OPT_PI: Icmp6NdOptPI}.get(self._frame[optr], Icmp6NdOptUnk)( - self._frame, optr - ) - ) - optr += self._frame[optr + 1] << 3 - - return nd_options - - @property - def nd_options(self): - """Read ND options""" - - if self.__nd_options is self.__not_cached: - assert self.type in {ICMP6_ROUTER_SOLICITATION, ICMP6_ROUTER_ADVERTISEMENT, ICMP6_NEIGHBOR_SOLICITATION, ICMP6_NEIGHBOR_ADVERTISEMENT} - optr = ( - self._hptr - + {ICMP6_ROUTER_SOLICITATION: 12, ICMP6_ROUTER_ADVERTISEMENT: 16, ICMP6_NEIGHBOR_SOLICITATION: 24, ICMP6_NEIGHBOR_ADVERTISEMENT: 24}[self.type] - ) - self.__nd_options = self._read_nd_options(optr) - return self.__nd_options - - @property - def nd_opt_slla(self): - """ICMPv6 ND option - Source Link Layer Address (1)""" - - if self.__nd_opt_slla is self.__not_cached: - assert self.type in {ICMP6_ROUTER_SOLICITATION, ICMP6_ROUTER_ADVERTISEMENT, ICMP6_NEIGHBOR_SOLICITATION, ICMP6_NEIGHBOR_ADVERTISEMENT} - for option in self.nd_options: - if option.code == ICMP6_ND_OPT_SLLA: - __nd_opt_slla = option.slla - break - else: - __nd_opt_slla = None - return __nd_opt_slla - - @property - def nd_opt_tlla(self): - """ICMPv6 ND option - Target Link Layer Address (2)""" - - if self.__nd_opt_tlla is self.__not_cached: - assert self.type in {ICMP6_ROUTER_SOLICITATION, ICMP6_ROUTER_ADVERTISEMENT, ICMP6_NEIGHBOR_SOLICITATION, ICMP6_NEIGHBOR_ADVERTISEMENT} - for option in self.nd_options: - if option.code == ICMP6_ND_OPT_TLLA: - __nd_opt_tlla = option.tlla - break - else: - __nd_opt_tlla = None - return __nd_opt_tlla - - @property - def nd_opt_pi(self): - """ICMPv6 ND option - Prefix Info (3) - Returns list of prefixes that can be used for address autoconfiguration""" - - if self.__nd_opt_pi is self.__not_cached: - assert self.type in {ICMP6_ROUTER_SOLICITATION, ICMP6_ROUTER_ADVERTISEMENT, ICMP6_NEIGHBOR_SOLICITATION, ICMP6_NEIGHBOR_ADVERTISEMENT} - __nd_opt_pi = [_.prefix for _ in self.nd_options if _.code == ICMP6_ND_OPT_PI and _.flag_a and _.prefix.prefixlen == 64] - return __nd_opt_pi - - @property - def plen(self): - """Calculate packet length""" - - return self._plen - - @property - def packet_copy(self): - """Read the whole packet""" - - if self.__packet_copy is self.__not_cached: - self.__packet_copy = self._frame[self._hptr : self._hptr + self.plen] - return self.__packet_copy - - def _nd_option_integrity_check(self, optr): - """Check integrity of ICMPv6 ND options""" - - while optr < len(self._frame): - if optr + 1 > len(self._frame): - return "ICMPv6 sanity check fail - wrong option length (I)" - if self._frame[optr + 1] == 0: - return "ICMPv6 sanity check fail - wrong option length (II)" - optr += self._frame[optr + 1] << 3 - if optr > len(self._frame): - return "ICMPv6 sanity check fail - wrong option length (III)" - - return False - - def _packet_integrity_check(self, pshdr_sum): - """Packet integrity check to be run on raw frame prior to parsing to make sure parsing is safe""" - - if not config.packet_integrity_check: - return False - - if inet_cksum(self._frame, self._hptr, self._plen, pshdr_sum): - return "ICMPv6 integrity - wrong packet checksum" - - if not ICMP6_HEADER_LEN <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (I)" - - if self._frame[0] == ICMP6_UNREACHABLE: - if not 12 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - - elif self._frame[0] in {ICMP6_ECHO_REQUEST, ICMP6_ECHO_REPLY}: - if not 8 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - - elif self._frame[0] == ICMP6_MLD2_QUERY: - if not 28 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - if self._plen != 28 + struct.unpack_from("! H", self._frame, self._hptr + 26)[0] * 16: - return "ICMPv6 integrity - wrong packet length (III)" - - elif self._frame[0] == ICMP6_ROUTER_SOLICITATION: - if not 8 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - if fail := self._nd_option_integrity_check(self._hptr + 8): - return fail - - elif self._frame[0] == ICMP6_ROUTER_ADVERTISEMENT: - if not 16 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - if fail := self._nd_option_integrity_check(self._hptr + 16): - return fail - - elif self._frame[0] == ICMP6_NEIGHBOR_SOLICITATION: - if not 24 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - if fail := self._nd_option_integrity_check(self._hptr + 24): - return fail - - elif self._frame[0] == ICMP6_NEIGHBOR_ADVERTISEMENT: - if 24 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - if fail := self._nd_option_integrity_check(self._hptr + 24): - return fail - - elif self._frame[0] == ICMP6_MLD2_REPORT: - if not 8 <= self._plen <= len(self): - return "ICMPv6 integrity - wrong packet length (II)" - optr = self._hptr + 8 - for _ in range(struct.unpack_from("! H", self._frame, self._hptr + 6)[0]): - if optr + 20 > self._hptr + self._plen: - return "ICMPv6 integrity - wrong packet length (III)" - optr += 20 + self._frame[optr + 1] + struct.unpack_from("! H", self._frame, optr + 2)[0] * 16 - if optr != self._hptr + self._plen: - return "ICMPv6 integrity - wrong packet length (IV)" - - return False - - def _packet_sanity_check(self, ip6_src, ip6_dst, ip6_hop): - """Packet sanity check to be run on parsed packet to make sure frame's fields contain sane values""" - - if not config.packet_sanity_check: - return False - - if self.type == ICMP6_UNREACHABLE: - if self.code not in {0, 1, 2, 3, 4, 5, 6}: - return "ICMPv6 sanity - 'code' must be [0-6] (RFC 4861)" - - elif self.type == ICMP6_PACKET_TOO_BIG: - if not self.code == 0: - return "ICMPv6 sanity - 'code' should be 0 (RFC 4861)" - - elif self.type == ICMP6_TIME_EXCEEDED: - if self.code not in {0, 1}: - return "ICMPv6 sanity - 'code' must be [0-1] (RFC 4861)" - - elif self.type == ICMP6_PARAMETER_PROBLEM: - if self.code not in {0, 1, 2}: - return "ICMPv6 sanity - 'code' must be [0-2] (RFC 4861)" - - elif self.type in {ICMP6_ECHO_REQUEST, ICMP6_ECHO_REPLY}: - if not self.code == 0: - return "ICMPv6 sanity - 'code' should be 0 (RFC 4861)" - - elif self.type == ICMP6_MLD2_QUERY: - if not self.code == 0: - return "ICMPv6 sanity - 'code' must be 0 (RFC 3810)" - if not ip6_hop == 1: - return "ICMPv6 sanity - 'hop' must be 255 (RFC 3810)" - - elif self.type == ICMP6_ROUTER_SOLICITATION: - if not self.code == 0: - return "ICMPv6 sanity - 'code' must be 0 (RFC 4861)" - if not ip6_hop == 255: - return "ICMPv6 sanity - 'hop' must be 255 (RFC 4861)" - if not (ip6_src.is_unicast or ip6_src.is_unspecified): - return "ICMPv6 sanity - 'src' must be unicast or unspecified (RFC 4861)" - if not ip6_dst == IPv6Address("ff02::2"): - return "ICMPv6 sanity - 'dst' must be all-routers (RFC 4861)" - if ip6_src.is_unspecified and self.nd_opt_slla: - return "ICMPv6 sanity - 'nd_opt_slla' must not be included if 'src' is unspecified (RFC 4861)" - - elif self.type == ICMP6_ROUTER_ADVERTISEMENT: - if not self.code == 0: - return "ICMPv6 sanity - 'code' must be 0 (RFC 4861)" - if not ip6_hop == 255: - return "ICMPv6 sanity - 'hop' must be 255 (RFC 4861)" - if not ip6_src.is_link_local: - return "ICMPv6 sanity - 'src' must be link local (RFC 4861)" - if not (ip6_dst.is_unicast or ip6_dst == IPv6Address("ff02::1")): - return "ICMPv6 sanity - 'dst' must be unicast or all-nodes (RFC 4861)" - - elif self.type == ICMP6_NEIGHBOR_SOLICITATION: - if not self.code == 0: - return "ICMPv6 sanity - 'code' must be 0 (RFC 4861)" - if not ip6_hop == 255: - return "ICMPv6 sanity - 'hop' must be 255 (RFC 4861)" - if not (ip6_src.is_unicast or ip6_src.is_unspecified): - return "ICMPv6 sanity - 'src' must be unicast or unspecified (RFC 4861)" - if ip6_dst not in {self.ns_target_address, self.ns_target_address.solicited_node_multicast}: - return "ICMPv6 sanity - 'dst' must be 'ns_target_address' or it's solicited-node multicast (RFC 4861)" - if not self.ns_target_address.is_unicast: - return "ICMPv6 sanity - 'ns_target_address' must be unicast (RFC 4861)" - if ip6_src.is_unspecified and self.nd_opt_slla is not None: - return "ICMPv6 sanity - 'nd_opt_slla' must not be included if 'src' is unspecified" - - elif self.type == ICMP6_NEIGHBOR_ADVERTISEMENT: - if not self.code == 0: - return "ICMPv6 sanity - 'code' must be 0 (RFC 4861)" - if not ip6_hop == 255: - return "ICMPv6 sanity - 'hop' must be 255 (RFC 4861)" - if not ip6_src.is_unicast: - return "ICMPv6 sanity - 'src' must be unicast (RFC 4861)" - if self.na_flag_s is True and not (ip6_dst.is_unicast or ip6_dst == IPv6Address("ff02::1")): - return "ICMPv6 sanity - if 'na_flag_s' is set then 'dst' must be unicast or all-nodes (RFC 4861)" - if self.na_flag_s is False and not ip6_dst == IPv6Address("ff02::1"): - return "ICMPv6 sanity - if 'na_flag_s' is not set then 'dst' must be all-nodes (RFC 4861)" - - elif self.type == ICMP6_MLD2_REPORT: - if not self.code == 0: - return "ICMPv6 sanity - 'code' must be 0 (RFC 3810)" - if not ip6_hop == 1: - return "ICMPv6 sanity - 'hop' must be 1 (RFC 3810)" - - return False - - -# -# ICMPv6 Neighbor Discovery options -# - - -# ICMPv6 ND option - Source Link Layer Address (1) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Length | > -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + -# > MAC Address | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -ICMP6_ND_OPT_SLLA = 1 -ICMP6_ND_OPT_SLLA_LEN = 8 - - -class Icmp6NdOptSLLA: - """ICMPv6 ND option - Source Link Layer Address (1)""" - - def __init__(self, frame, optr): - self.code = frame[optr + 0] - self.len = frame[optr + 1] << 3 - self.slla = ":".join([f"{_:0>2x}" for _ in frame[optr + 2 : optr + 8]]) - - def __str__(self): - return f"slla {self.slla}" - - -# ICMPv6 ND option - Target Link Layer Address (2) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Length | > -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + -# > MAC Address | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -ICMP6_ND_OPT_TLLA = 2 -ICMP6_ND_OPT_TLLA_LEN = 8 - - -class Icmp6NdOptTLLA: - """ICMPv6 ND option - Target Link Layer Address (2)""" - - def __init__(self, frame, optr): - self.code = frame[optr + 0] - self.len = frame[optr + 1] << 3 - self.tlla = ":".join([f"{_:0>2x}" for _ in frame[optr + 2 : optr + 8]]) - - def __str__(self): - return f"tlla {self.tlla}" - - -# ICMPv6 ND option - Prefix Information (3) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Type | Length | Prefix Length |L|A|R| Res1 | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Valid Lifetime | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Preferred Lifetime | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Reserved2 | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | | -# + + -# | | -# + Prefix + -# | | -# + + -# | | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -ICMP6_ND_OPT_PI = 3 -ICMP6_ND_OPT_PI_LEN = 32 - - -class Icmp6NdOptPI: - """ICMPv6 ND option - Prefix Information (3)""" - - def __init__(self, frame, optr): - self.code = frame[optr + 0] - self.len = frame[optr + 1] << 3 - self.flag_l = bool(frame[optr + 3] & 0b10000000) - self.flag_a = bool(frame[optr + 3] & 0b01000000) - self.flag_r = bool(frame[optr + 3] & 0b00100000) - self.valid_lifetime = struct.unpack_from("!L", frame, optr + 4)[0] - self.preferred_lifetime = struct.unpack_from("!L", frame, optr + 8)[0] - self.prefix = IPv6Network((frame[optr + 16 : optr + 32], frame[optr + 2])) - - def __str__(self): - return f"prefix_info {self.prefix}" - - -# ICMPv6 ND option not supported by this stack - - -class Icmp6NdOptUnk: - """ICMPv6 ND option not supported by this stack""" - - def __init__(self, frame, optr): - self.code = frame[optr + 0] - self.len = frame[optr + 1] << 3 - self.data = frame[optr + 2 : optr + self.len] - - def __str__(self): - return f"unk-{self.code}-{self.len}" - - -# -# ICMPv6 Multicast support classes -# - - -class MulticastAddressRecord: - """Multicast Address Record used by MLDv2 Report message""" - - def __init__(self, raw_record=None, record_type=None, multicast_address=None, source_address=None, aux_data=b""): - """Class constructor""" - - # Record parsing - if raw_record: - self.record_type = raw_record[0] - self.aux_data_len = raw_record[1] - self.number_of_sources = struct.unpack("!H", raw_record[2:4])[0] - self.multicast_address = IPv6Address(raw_record[4:20]) - self.source_address = [IPv6Address(raw_record[20 + 16 * _ : 20 + 16 * (_ + 1)]) for _ in range(self.number_of_sources)] - self.aux_data = raw_record[20 + 16 * self.number_of_sources :] - - # Record building - else: - self.record_type = record_type - self.aux_data_len = len(aux_data) - self.multicast_address = IPv6Address(multicast_address) - self.source_address = [] if source_address is None else source_address - self.number_of_sources = len(self.source_address) - self.aux_data = aux_data - - def __len__(self): - """Length of raw record""" - - return len(self.raw_record) - - def __hash__(self): - """Hash of raw record""" - - return hash(self.raw_record) - - def __eq__(self, other): - """Compare two records""" - - return self.raw_record == other.raw_record - - @property - def raw_record(self): - """Get record in raw format""" - - return ( - struct.pack("! BBH 16s", self.record_type, self.aux_data_len, self.number_of_sources, self.multicast_address.packed) - + b"".join([_.packed for _ in self.source_address]) - + self.aux_data - ) diff --git a/fpp_ip4.py b/fpp_ip4.py deleted file mode 100755 index 4057459b..00000000 --- a/fpp_ip4.py +++ /dev/null @@ -1,432 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpp_ip4.py - Fast Packet Parser support class for IPv4 protocol -# - - -import struct - -import config -from ip_helper import inet_cksum -from ipv4_address import IPv4Address - -# IPv4 protocol header - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# |Version| IHL | DSCP |ECN| Packet length | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Identification |Flags| Fragment offset | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Time to live | Protocol | Header checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Source address | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Destination address | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Options ~ Padding ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -IP4_HEADER_LEN = 20 - -IP4_PROTO_ICMP4 = 1 -IP4_PROTO_TCP = 6 -IP4_PROTO_UDP = 17 - - -IP4_PROTO_TABLE = {IP4_PROTO_ICMP4: "ICMPv4", IP4_PROTO_TCP: "TCP", IP4_PROTO_UDP: "UDP"} - - -class Ip4Packet: - """IPv4 packet support class""" - - class __not_cached: - pass - - def __init__(self, packet_rx): - """Class constructor""" - - packet_rx.ip4 = self - packet_rx.ip = self - - self._frame = packet_rx.frame - self._hptr = packet_rx.hptr - - self.__ver = self.__not_cached - self.__hlen = self.__not_cached - self.__dscp = self.__not_cached - self.__ecn = self.__not_cached - self.__plen = self.__not_cached - self.__id = self.__not_cached - self.__offset = self.__not_cached - self.__cksum = self.__not_cached - self.__src = self.__not_cached - self.__dst = self.__not_cached - self.__options = self.__not_cached - self.__header_copy = self.__not_cached - self.__options_copy = self.__not_cached - self.__data_copy = self.__not_cached - self.__packet_copy = self.__not_cached - self.__olen = self.__not_cached - self.__dlen = self.__not_cached - self.__packet = self.__not_cached - self.__pshdr_sum = self.__not_cached - - packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() - - if not packet_rx.parse_failed: - packet_rx.hptr = self._hptr + self.hlen - - def __str__(self): - """Packet log string""" - - return ( - f"IPv4 {self.src} > {self.dst}, proto {self.proto} ({IP4_PROTO_TABLE.get(self.proto, '???')}), id {self.id}" - + f"{', DF' if self.flag_df else ''}{', MF' if self.flag_mf else ''}, offset {self.offset}, plen {self.plen}" - + f", ttl {self.ttl}" - ) - - def __len__(self): - """Number of bytes remaining in the frame""" - - return len(self._frame) - self._hptr - - @property - def ver(self): - """Read 'Version' field""" - - if self.__ver is self.__not_cached: - self.__ver = self._frame[self._hptr + 0] >> 4 - return self.__ver - - @property - def hlen(self): - """Read 'Header length' field""" - - if self.__hlen is self.__not_cached: - self.__hlen = (self._frame[self._hptr + 0] & 0b00001111) << 2 - return self.__hlen - - @property - def dscp(self): - """Read 'DSCP' field""" - - if self.__dscp is self.__not_cached: - self.__dscp = (self._frame[self._hptr + 1] & 0b11111100) >> 2 - return self.__dscp - - @property - def ecn(self): - """Read 'ECN' field""" - - if self.__ecn is self.__not_cached: - self.__ecn = self._frame[self._hptr + 1] & 0b00000011 - return self.__ecn - - @property - def plen(self): - """Read 'Packet length' field""" - - if self.__plen is self.__not_cached: - self.__plen = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] - return self.__plen - - @property - def id(self): - """Read 'Identification' field""" - - if self.__id is self.__not_cached: - self.__id = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] - return self.__id - - @property - def flag_df(self): - """Read 'DF flag' field""" - - return self._frame[self._hptr + 6] & 0b01000000 - - @property - def flag_mf(self): - """Read 'MF flag' field""" - - return self._frame[self._hptr + 6] & 0b00100000 - - @property - def offset(self): - """Read 'Fragment offset' field""" - - if self.__offset is self.__not_cached: - self.__offset = (struct.unpack_from("!H", self._frame, self._hptr + 6)[0] & 0b0001111111111111) << 3 - return self.__offset - - @property - def ttl(self): - """Read 'TTL' field""" - - return self._frame[self._hptr + 8] - - @property - def proto(self): - """Read 'Protocol' field""" - - return self._frame[self._hptr + 9] - - @property - def cksum(self): - """Read 'Checksum' field""" - - if self.__cksum is self.__not_cached: - self.__cksum = struct.unpack_from("!H", self._frame, self._hptr + 10)[0] - return self.__cksum - - @property - def src(self): - """Read 'Source address' field""" - - if self.__src is self.__not_cached: - self.__src = IPv4Address(self._frame[self._hptr + 12 : self._hptr + 16]) - return self.__src - - @property - def dst(self): - """Read 'Destination address' field""" - - if self.__dst is self.__not_cached: - self.__dst = IPv4Address(self._frame[self._hptr + 16 : self._hptr + 20]) - return self.__dst - - @property - def options(self): - """Read list of options""" - - if self.__options is self.__not_cached: - self.__options = [] - optr = self._hptr + IP4_HEADER_LEN - - while optr < self._hptr + self.hlen: - if self._frame[optr] == IP4_OPT_EOL: - self.__options.append(Ip4OptEol()) - break - if self._frame[optr] == IP4_OPT_NOP: - self.__options.append(Ip4OptNop()) - optr += IP4_OPT_NOP_LEN - continue - self.__options.append({}.get(self._frame[optr], Ip4OptUnk)(self._frame, optr)) - optr += self._frame[optr + 1] - - return self.__options - - @property - def olen(self): - """Calculate options length""" - - if self.__olen is self.__not_cached: - self.__olen = self.hlen - IP4_HEADER_LEN - return self.__olen - - @property - def dlen(self): - """Calculate data length""" - - if self.__dlen is self.__not_cached: - self.__dlen = self.plen - self.hlen - return self.__dlen - - @property - def header_copy(self): - """Return copy of packet header""" - - if self.__header_copy is self.__not_cached: - self.__header_copy = self._frame[self._hptr : self._hptr + IP4_HEADER_LEN] - return self.__header_copy - - @property - def options_copy(self): - """Return copy of packet header""" - - if self.__options_copy is self.__not_cached: - self.__options_copy = self._frame[self._hptr + IP4_HEADER_LEN : self._hptr + self.hlen] - return self.__options_copy - - @property - def data_copy(self): - """Return copy of packet data""" - - if self.__data_copy is self.__not_cached: - self.__data_copy = self._frame[self._hptr + self.hlen : self._hptr + self.plen] - return self.__data_copy - - @property - def packet_copy(self): - """Return copy of whole packet""" - - if self.__packet_copy is self.__not_cached: - self.__packet_copy = self._frame[self._hptr : self._hptr + self.plen] - return self.__packet_copy - - @property - def pshdr_sum(self): - """Create IPv4 pseudo header used by TCP and UDP to compute their checksums""" - - if self.__pshdr_sum is self.__not_cached: - pseudo_header = struct.pack("! 4s 4s BBH", self.src.packed, self.dst.packed, 0, self.proto, self.plen - self.hlen) - self.__pshdr_sum = sum(struct.unpack("! 3L", pseudo_header)) - return self.__pshdr_sum - - def _packet_integrity_check(self): - """Packet integrity check to be run on raw packet prior to parsing to make sure parsing is safe""" - - if not config.packet_integrity_check: - return False - - if len(self) < IP4_HEADER_LEN: - return "IPv4 integrity - wrong packet length (I)" - - hlen = (self._frame[self._hptr + 0] & 0b00001111) << 2 - plen = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] - if not IP4_HEADER_LEN <= hlen <= plen <= len(self): - return "IPv4 integrity - wrong packet length (II)" - - # Cannot compute checksum earlier because it depends on sanity of hlen field - if inet_cksum(self._frame, self._hptr, hlen): - return "IPv4 integriy - wrong packet checksum" - - optr = self._hptr + IP4_HEADER_LEN - while optr < self._hptr + hlen: - if self._frame[optr] == IP4_OPT_EOL: - break - if self._frame[optr] == IP4_OPT_NOP: - optr += 1 - if optr > self._hptr + hlen: - return "IPv4 integrity - wrong option length (I)" - continue - if optr + 1 > self._hptr + hlen: - return "IPv4 integrity - wrong option length (II)" - if self._frame[optr + 1] == 0: - return "IPv4 integrity - wrong option length (III)" - optr += self._frame[optr + 1] - if optr > self._hptr + hlen: - return "IPv4 integrity - wrong option length (IV)" - - return False - - def _packet_sanity_check(self): - """Packet sanity check to be run on parsed packet to make sure packet's fields contain sane values""" - - if not config.packet_sanity_check: - return False - - if self.ver != 4: - return "IP sanityi - 'ver' must be 4" - - if self.ver == 0: - return "IP sanity - 'ttl' must be greater than 0" - - if self.src.is_multicast: - return "IP sanity - 'src' must not be multicast" - - if self.src.is_reserved: - return "IP sanity - 'src' must not be reserved" - - if self.src.is_limited_broadcast: - return "IP sanity - 'src' must not be limited broadcast" - - if self.flag_df and self.flag_mf: - return "IP sanity - 'flag_df' and 'flag_mf' must not be set simultaneously" - - if self.offset and self.flag_df: - return "IP sanity - 'offset' must be 0 when 'df_flag' is set" - - if self.options and config.ip4_option_packet_drop: - return "IP sanity - packet must not contain options" - - return False - - -# -# IPv4 options -# - - -# IPv4 option - End of Option Linst - -IP4_OPT_EOL = 0 -IP4_OPT_EOL_LEN = 1 - - -class Ip4OptEol: - """IP option - End of Option List""" - - def __init__(self): - self.kind = IP4_OPT_EOL - - def __str__(self): - return "eol" - - -# IPv4 option - No Operation (1) - -IP4_OPT_NOP = 1 -IP4_OPT_NOP_LEN = 1 - - -class Ip4OptNop: - """IP option - No Operation""" - - def __init__(self): - self.kind = IP4_OPT_NOP - - def __str__(self): - return "nop" - - -# IPv4 option not supported by this stack - - -class Ip4OptUnk: - """IP option not supported by this stack""" - - def __init__(self, frame, optr): - self.kind = frame[optr + 0] - self.len = frame[optr + 1] - self.data = frame[optr + 2 : optr + self.len] - - def __str__(self): - return f"unk-{self.kind}-{self.len}" diff --git a/fpp_ip6.py b/fpp_ip6.py deleted file mode 100755 index f4afa19d..00000000 --- a/fpp_ip6.py +++ /dev/null @@ -1,272 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpp_ip6.py - packet parser IPv6 protocol -# - - -import struct - -import config -from ipv6_address import IPv6Address - -# IPv6 protocol header - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# |Version| Traffic Class | Flow Label | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Payload Length | Next Header | Hop Limit | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + + -# > > -# + Source Address + -# > > -# + + -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | > -# + + -# > > -# + Destination Address + -# > > -# + + -# > | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -IP6_HEADER_LEN = 40 - -IP6_NEXT_HEADER_TCP = 6 -IP6_NEXT_HEADER_UDP = 17 -IP6_NEXT_HEADER_EXT_FRAG = 44 -IP6_NEXT_HEADER_ICMP6 = 58 - -IP6_NEXT_HEADER_TABLE = {IP6_NEXT_HEADER_TCP: "TCP", IP6_NEXT_HEADER_UDP: "UDP", IP6_NEXT_HEADER_EXT_FRAG: "IP6_FRAG", IP6_NEXT_HEADER_ICMP6: "ICMPv6"} - - -class Ip6Packet: - """IPv6 packet support class""" - - class __not_cached: - pass - - def __init__(self, packet_rx): - """Class constructor""" - - packet_rx.ip6 = self - packet_rx.ip = self - - self._frame = packet_rx.frame - self._hptr = packet_rx.hptr - - self.__ver = self.__not_cached - self.__dscp = self.__not_cached - self.__ecn = self.__not_cached - self.__flow = self.__not_cached - self.__dlen = self.__not_cached - self.__src = self.__not_cached - self.__dst = self.__not_cached - self.__data = self.__not_cached - self.__header_copy = self.__not_cached - self.__data_copy = self.__not_cached - self.__packet_copy = self.__not_cached - self.__pshdr_sum = self.__not_cached - - packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() - - if not packet_rx.parse_failed: - packet_rx.hptr = self._hptr + IP6_HEADER_LEN - - def __str__(self): - """Packet log string""" - - return ( - f"IPv6 {self.src} > {self.dst}, next {self.next} ({IP6_NEXT_HEADER_TABLE.get(self.next, '???')}), flow {self.flow}" - + f", dlen {self.dlen}, hop {self.hop}" - ) - - def __len__(self): - """Number of bytes remaining in the frame""" - - return len(self._frame) - self._hptr - - @property - def ver(self): - """Read 'Version' field""" - - if self.__ver is self.__not_cached: - self.__ver = self._frame[self._hptr + 0] >> 4 - return self.__ver - - @property - def dscp(self): - """Read 'DSCP' field""" - - if self.__dscp is self.__not_cached: - self.__dscp = ((self._frame[self._hptr + 0] & 0b00001111) << 2) | ((self._frame[self._hptr + 1] & 0b11000000) >> 6) - return self.__dscp - - @property - def ecn(self): - """Read 'ECN' field""" - - if self.__ecn is self.__not_cached: - self.__ecn = (self._frame[self._hptr + 1] & 0b00110000) >> 4 - return self.__ecn - - @property - def flow(self): - """Read 'Flow' field""" - - if self.__flow is self.__not_cached: - self.__flow = ((self._frame[self._hptr + 1] & 0b00001111) << 16) | (self._frame[self._hptr + 2] << 8) | self._frame[self._hptr + 3] - return self.__flow - - @property - def dlen(self): - """Read 'Data length' field""" - - if self.__dlen is self.__not_cached: - self.__dlen = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] - return self.__dlen - - @property - def next(self): - """Read 'Next' field""" - - return self._frame[self._hptr + 6] - - @property - def hop(self): - """Read 'Hop' field""" - - return self._frame[self._hptr + 7] - - @property - def src(self): - """Read 'Source address' field""" - - if self.__src is self.__not_cached: - self.__src = IPv6Address(self._frame[self._hptr + 8 : self._hptr + 24]) - return self.__src - - @property - def dst(self): - """Read 'Destination address' field""" - - if self.__dst is self.__not_cached: - self.__dst = IPv6Address(self._frame[self._hptr + 24 : self._hptr + 40]) - return self.__dst - - @property - def hlen(self): - """Calculate header length""" - - return IP6_HEADER_LEN - - @property - def plen(self): - """Calculate packet length""" - - return IP6_HEADER_LEN + self.dlen - - @property - def header_copy(self): - """Return copy of packet header""" - - if self.__header_copy is self.__not_cached: - self.__header_copy = self._frame[self._hptr : self._hptr + IP6_HEADER_LEN] - return self.__header_copy - - @property - def data_copy(self): - """Return copy of packet data""" - - if self.__data_copy is self.__not_cached: - self.__data_copy = self._frame[self._hptr + IP6_HEADER_LEN : self._hptr + self.plen] - return self.__data_copy - - @property - def packet_copy(self): - """Return copy of whole packet""" - - if self.__packet_copy is self.__not_cached: - self.__packet_copy = self._frame[self._hptr : self._hptr + self.plen] - return self.__packet_copy - - @property - def pshdr_sum(self): - """Returns IPv6 pseudo header that is used by TCP, UDP and ICMPv6 to compute their checksums""" - - if self.__pshdr_sum is self.__not_cached: - pseudo_header = struct.pack("! 16s 16s L BBBB", self.src.packed, self.dst.packed, self.dlen, 0, 0, 0, self.next) - self.__pshdr_sum = sum(struct.unpack("! 5Q", pseudo_header)) - return self.__pshdr_sum - - def _packet_integrity_check(self): - """Packet integrity check to be run on raw packet prior to parsing to make sure parsing is safe""" - - if not config.packet_integrity_check: - return False - - if len(self) < IP6_HEADER_LEN: - return "IPv6 integrity - wrong packet length (I)" - - if struct.unpack_from("!H", self._frame, self._hptr + 4)[0] != len(self) - IP6_HEADER_LEN: - return "IPv6 integrity - wrong packet length (II)" - - return False - - def _packet_sanity_check(self): - """Packet sanity check to be run on parsed packet to make sure packet's fields contain sane values""" - - if not config.packet_sanity_check: - return False - - if self.ver != 6: - return "IPv6 sanity - 'ver' must be 6" - - if self.hop == 0: - return "IPv6 sanity - 'hop' must not be 0" - - if self.src.is_multicast: - return "IPv6 sanity - 'src' must not be multicast" - - return False diff --git a/fpp_tcp.py b/fpp_tcp.py deleted file mode 100755 index cc1a5636..00000000 --- a/fpp_tcp.py +++ /dev/null @@ -1,599 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpp_tcp.py - Fast Packet Parser support class for TCP protocol -# - - -import struct - -import config -from ip_helper import inet_cksum - -# TCP packet header (RFC 793) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Source Port | Destination Port | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Sequence Number | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Acknowledgment Number | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Hlen | Res |N|C|E|U|A|P|R|S|F| Window | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Checksum | Urgent Pointer | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Options ~ Padding ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -TCP_HEADER_LEN = 20 - - -class TcpPacket: - """TCP packet support class""" - - class __not_cached: - pass - - def __init__(self, packet_rx): - """Class constructor""" - - packet_rx.tcp = self - - self._frame = packet_rx.frame - self._hptr = packet_rx.hptr - self._plen = packet_rx.ip.dlen - - self.__sport = self.__not_cached - self.__dport = self.__not_cached - self.__seq = self.__not_cached - self.__ack = self.__not_cached - self.__hlen = self.__not_cached - self.__flag_ns = self.__not_cached - self.__flag_crw = self.__not_cached - self.__flag_ece = self.__not_cached - self.__flag_urg = self.__not_cached - self.__flag_ack = self.__not_cached - self.__flag_psh = self.__not_cached - self.__flag_rst = self.__not_cached - self.__flag_syn = self.__not_cached - self.__flag_fin = self.__not_cached - self.__win = self.__not_cached - self.__cksum = self.__not_cached - self.__urg = self.__not_cached - self.__data = self.__not_cached - self.__olen = self.__not_cached - self.__options = self.__not_cached - self.__header_copy = self.__not_cached - self.__options_copy = self.__not_cached - self.__data_copy = self.__not_cached - self.__packet_copy = self.__not_cached - self.__mss = self.__not_cached - self.__wscale = self.__not_cached - self.__sackperm = self.__not_cached - self.__timestamp = self.__not_cached - - packet_rx.parse_failed = self._packet_integrity_check(packet_rx.ip.pshdr_sum) or self._packet_sanity_check() - - if packet_rx.parse_failed: - packet_rx.hptr = self._hptr + self.hlen - - def __str__(self): - """Packet log string""" - - log = ( - f"TCP {self.sport} > {self.dport}, {'N' if self.flag_ns else ''}{'C' if self.flag_crw else ''}" - + f"{'E' if self.flag_ece else ''}{'U' if self.flag_urg else ''}{'A' if self.flag_ack else ''}" - + f"{'P' if self.flag_psh else ''}{'R' if self.flag_rst else ''}{'S' if self.flag_syn else ''}" - + f"{'F' if self.flag_fin else ''}, seq {self.seq}, ack {self.ack}, win {self.win}, dlen {self.dlen}" - ) - - for option in self.options: - log += ", " + str(option) - - return log - - def __len__(self): - """Packet length""" - - return len(self._frame) - self._hptr - - @property - def sport(self): - """Read 'Source port' field""" - - if self.__sport is self.__not_cached: - self.__sport = struct.unpack_from("!H", self._frame, self._hptr + 0)[0] - return self.__sport - - @property - def dport(self): - """Read 'Destianation port' field""" - - if self.__dport is self.__not_cached: - self.__dport = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] - return self.__dport - - @property - def seq(self): - """Read 'Sequence number' field""" - - if self.__seq is self.__not_cached: - self.__seq = struct.unpack_from("!L", self._frame, self._hptr + 4)[0] - return self.__seq - - @property - def ack(self): - """Read 'Acknowledge number' field""" - - if self.__ack is self.__not_cached: - self.__ack = struct.unpack_from("!L", self._frame, self._hptr + 8)[0] - return self.__ack - - @property - def hlen(self): - """Read 'Header length' field""" - - if self.__hlen is self.__not_cached: - self.__hlen = (self._frame[self._hptr + 12] & 0b11110000) >> 2 - return self.__hlen - - @property - def flag_ns(self): - """Read 'NS flag' field""" - - if self.__flag_ns is self.__not_cached: - self.__flag_ns = bool(self._frame[self._hptr + 12] & 0b00000001) - return self.__flag_ns - - @property - def flag_crw(self): - """Read 'CRW flag' field""" - - if self.__flag_crw is self.__not_cached: - self.__flag_crw = bool(self._frame[self._hptr + 13] & 0b10000000) - return self.__flag_crw - - @property - def flag_ece(self): - """Read 'ECE flag' field""" - - if self.__flag_ece is self.__not_cached: - self.__flag_ece = bool(self._frame[self._hptr + 13] & 0b01000000) - return self.__flag_ece - - @property - def flag_urg(self): - """Read 'URG flag' field""" - - if self.__flag_urg is self.__not_cached: - self.__flag_urg = bool(self._frame[self._hptr + 13] & 0b00100000) - return self.__flag_urg - - @property - def flag_ack(self): - """Read 'ACK flag' field""" - - if self.__flag_ack is self.__not_cached: - self.__flag_ack = bool(self._frame[self._hptr + 13] & 0b00010000) - return self.__flag_ack - - @property - def flag_psh(self): - """Read 'PSH flag' field""" - - if self.__flag_psh is self.__not_cached: - self.__flag_psh = bool(self._frame[self._hptr + 13] & 0b00001000) - return self.__flag_psh - - @property - def flag_rst(self): - """Read 'RST flag' field""" - - if self.__flag_rst is self.__not_cached: - self.__flag_rst = bool(self._frame[self._hptr + 13] & 0b00000100) - return self.__flag_rst - - @property - def flag_syn(self): - """Read 'SYN flag' field""" - - if self.__flag_syn is self.__not_cached: - self.__flag_syn = bool(self._frame[self._hptr + 13] & 0b00000010) - return self.__flag_syn - - @property - def flag_fin(self): - """Read 'FIN flag' field""" - - if self.__flag_fin is self.__not_cached: - self.__flag_fin = bool(self._frame[self._hptr + 13] & 0b00000001) - return self.__flag_fin - - @property - def win(self): - """Read 'Window' field""" - - if self.__win is self.__not_cached: - self.__win = struct.unpack_from("!H", self._frame, self._hptr + 14)[0] - return self.__win - - @property - def cksum(self): - """Read 'Checksum' field""" - - if self.__cksum is self.__not_cached: - self.__cksum = struct.unpack_from("!H", self._frame, self._hptr + 16)[0] - return self.__cksum - - @property - def urg(self): - """Read 'Urgent pointer' field""" - - if self.__urg is self.__not_cached: - self.__urg = struct.unpack_from("!H", self._frame, self._hptr + 18)[0] - return self.__urg - - @property - def data(self): - """Read the data packet carries""" - - if self.__data is self.__not_cached: - self.__data = self._frame[self._hptr + self.hlen : self._hptr + self.plen] - return self.__data - - @property - def olen(self): - """Calculate options length""" - - if self.__olen is self.__not_cached: - self.__olen = self.hlen - TCP_HEADER_LEN - return self.__olen - - @property - def dlen(self): - """Calculate data length""" - - return self._plen - self.hlen - - @property - def plen(self): - """Calculate packet length""" - - return self._plen - - @property - def header_copy(self): - """Return copy of packet header""" - - if self.__header_copy is self.__not_cached: - self.__header_copy = self._frame[self._hptr : self._hptr + TCP_HEADER_LEN] - return self.__header_copy - - @property - def options_copy(self): - """Return copy of packet header""" - - if self.__options_copy is self.__not_cached: - self.__options_copy = self._frame[self._hptr + TCP_HEADER_LEN : self._hptr + self.hlen] - return self.__options_copy - - @property - def data_copy(self): - """Return copy of packet data""" - - if self.__data_copy is self.__not_cached: - self.__data_copy = self._frame[self._hptr + self.hlen : self._hptr + self.plen] - return self.__data_copy - - @property - def packet_copy(self): - """Return copy of whole packet""" - - if self.__packet_copy is self.__not_cached: - self.__packet_copy = self._frame[self._hptr : self._hptr + self.plen] - return self.__packet_copy - - @property - def options(self): - """Read list of options""" - - if self.__options is self.__not_cached: - self.__options = [] - optr = self._hptr + TCP_HEADER_LEN - while optr < self._hptr + self.hlen: - if self._frame[optr] == TCP_OPT_EOL: - self.__options.append(TcpOptEol()) - break - if self._frame[optr] == TCP_OPT_NOP: - self.__options.append(TcpOptNop()) - optr += TCP_OPT_NOP_LEN - continue - self.__options.append( - {TCP_OPT_MSS: TcpOptMss, TCP_OPT_WSCALE: TcpOptWscale, TCP_OPT_SACKPERM: TcpOptSackPerm, TCP_OPT_TIMESTAMP: TcpOptTimestamp}.get( - self._frame[optr], TcpOptUnk - )(self._frame, optr) - ) - optr += self._frame[optr + 1] - - return self.__options - - @property - def mss(self): - """TCP option - Maximum Segment Size (2)""" - - if self.__mss is self.__not_cached: - for option in self.options: - if option.kind == TCP_OPT_MSS: - self.__mss = option.mss - break - else: - self.__mss = 536 - return self.__mss - - @property - def wscale(self): - """TCP option - Window Scale (3)""" - - if self.__wscale is self.__not_cached: - for option in self.options: - if option.kind == TCP_OPT_WSCALE: - self.__wscale = 1 << option.wscale - break - else: - self.__wscale = None - return self.__wscale - - @property - def sackperm(self): - """TCP option - Sack Permit (4)""" - - if self.__sackperm is self.__not_cached: - for option in self.options: - if option.kind == TCP_OPT_SACKPERM: - self.__sackperm = True - break - else: - self.__sackperm = None - return self.__sackperm - - @property - def timestamp(self): - """TCP option - Timestamp (8)""" - - if self.__timestamp is self.__not_cached: - for option in self.options: - if option.kind == TCP_OPT_TIMESTAMP: - self.__timestamp = (option.tsval, option.tsecr) - break - else: - self.__timestamp = None - return self.__timestamp - - def _packet_integrity_check(self, pshdr_sum): - """Packet integrity check to be run on raw frame prior to parsing to make sure parsing is safe""" - - if not config.packet_integrity_check: - return False - - if inet_cksum(self._frame, self._hptr, self._plen, pshdr_sum): - return "TCP integrity - wrong packet checksum" - - if not TCP_HEADER_LEN <= self._plen <= len(self): - return "TCP integrity - wrong packet length (I)" - - hlen = (self._frame[self._hptr + 12] & 0b11110000) >> 2 - if not TCP_HEADER_LEN <= hlen <= self._plen <= len(self): - return "TCP integrity - wrong packet length (II)" - - optr = self._hptr + TCP_HEADER_LEN - while optr < self._hptr + hlen: - if self._frame[optr] == TCP_OPT_EOL: - break - if self._frame[optr] == TCP_OPT_NOP: - optr += 1 - if optr > self._hptr + hlen: - return "TCP integrity - wrong option length (I)" - continue - if optr + 1 > self._hptr + hlen: - return "TCP integrity - wrong option length (II)" - if self._frame[optr + 1] == 0: - return "TCP integrity - wrong option length (III)" - optr += self._frame[optr + 1] - if optr > self._hptr + hlen: - return "TCP integrity - wrong option length (IV)" - - return False - - def _packet_sanity_check(self): - """Packet sanity check to be run on parsed packet to make sure frame's fields contain sane values""" - - if not config.packet_sanity_check: - return False - - if self.sport == 0: - return "TCP sanity - 'sport' must be greater than 0" - - if self.dport == 0: - return "TCP sanity - 'dport' must be greater than 0" - - if self.flag_syn and self.flag_fin: - return "TCP sanity - 'flag_syn' and 'flag_fin' must not be set simultaneously" - - if self.flag_syn and self.flag_rst: - return "TCP sanity - 'flag_syn' and 'flag_rst' must not set simultaneously" - - if self.flag_fin and self.flag_rst: - return "TCP sanity - 'flag_fin' and 'flag_rst' must not be set simultaneously" - - if self.flag_fin and not self.flag_ack: - return "TCP sanity - 'flag_ack' must be set when 'flag_fin' is set" - - if self.ack and not self.flag_ack: - return "TCP sanity - 'flag_ack' must be set when 'ack' is not 0" - - if self.urg and not self.flag_urg: - return "TCP sanity - 'flag_urg' must be set when 'urg' is not 0" - - return False - - -# -# TCP options -# - - -# TCP option - End of Option List (0) - -TCP_OPT_EOL = 0 -TCP_OPT_EOL_LEN = 1 - - -class TcpOptEol: - """TCP option - End of Option List (0)""" - - def __init__(self): - self.kind = TCP_OPT_EOL - - def __str__(self): - return "eol" - - -# TCP option - No Operation (1) - -TCP_OPT_NOP = 1 -TCP_OPT_NOP_LEN = 1 - - -class TcpOptNop: - """TCP option - No Operation (1)""" - - def __init__(self): - self.kind = TCP_OPT_NOP - - def __str__(self): - return "nop" - - -# TCP option - Maximum Segment Size (2) - -TCP_OPT_MSS = 2 -TCP_OPT_MSS_LEN = 4 - - -class TcpOptMss: - """TCP option - Maximum Segment Size (2)""" - - def __init__(self, frame, optr): - self.kind = frame[optr + 0] - self.len = frame[optr + 1] - self.mss = struct.unpack_from("!H", frame, optr + 2)[0] - - def __str__(self): - return f"mss {self.mss}" - - -# TCP option - Window Scale (3) - -TCP_OPT_WSCALE = 3 -TCP_OPT_WSCALE_LEN = 3 - - -class TcpOptWscale: - """TCP option - Window Scale (3)""" - - def __init__(self, frame, optr): - self.kind = frame[optr + 0] - self.len = frame[optr + 1] - self.wscale = frame[optr + 2] - - def __str__(self): - return f"wscale {self.wscale}" - - -# TCP option - Sack Permit (4) - -TCP_OPT_SACKPERM = 4 -TCP_OPT_SACKPERM_LEN = 2 - - -class TcpOptSackPerm: - """TCP option - Sack Permit (4)""" - - def __init__(self, frame, optr): - self.kind = frame[optr + 0] - self.len = frame[optr + 1] - - def __str__(self): - return "sack_perm" - - -# TCP option - Timestamp - -TCP_OPT_TIMESTAMP = 8 -TCP_OPT_TIMESTAMP_LEN = 10 - - -class TcpOptTimestamp: - """TCP option - Timestamp (8)""" - - def __init__(self, frame, optr): - self.kind = frame[optr + 0] - self.len = frame[optr + 1] - self.tsval = struct.unpack_from("!L", frame, optr + 2)[0] - self.tsecr = struct.unpack_from("!L", frame, optr + 6)[0] - - def __str__(self): - return f"ts {self.tsval}/{self.tsecr}" - - -# TCP option not supported by this stack - - -class TcpOptUnk: - """TCP option not supported by this stack""" - - def __init__(self, frame, optr): - self.kind = frame[optr + 0] - self.len = frame[optr + 1] - self.data = frame[optr + 2 : optr + self.len] - - def __str__(self): - return f"unk-{self.kind}-{self.len}" diff --git a/fpp_udp.py b/fpp_udp.py deleted file mode 100755 index 8e2f173f..00000000 --- a/fpp_udp.py +++ /dev/null @@ -1,182 +0,0 @@ -#!/usr/bin/env python3 - -############################################################################ -# # -# PyTCP - Python TCP/IP stack # -# Copyright (C) 2020-2021 Sebastian Majewski # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# Author's email: ccie18643@gmail.com # -# Github repository: https://github.com/ccie18643/PyTCP # -# # -############################################################################ - -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - - -# -# fpp_udp.py - Fast Packet Parser support class for UDP protocol -# - - -import struct - -import config -from ip_helper import inet_cksum - -# UDP packet header (RFC 768) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Source port | Destination port | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Packet length | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -UDP_HEADER_LEN = 8 - - -class UdpPacket: - """UDP packet support class""" - - class __not_cached: - pass - - def __init__(self, packet_rx): - """Class constructor""" - - packet_rx.udp = self - - self._frame = packet_rx.frame - self._hptr = packet_rx.hptr - self._plen = packet_rx.ip.dlen - - self.__sport = self.__not_cached - self.__dport = self.__not_cached - self.__plen = self.__not_cached - self.__cksum = self.__not_cached - self.__data = self.__not_cached - self.__packet = self.__not_cached - - packet_rx.parse_failed = self._packet_integrity_check(packet_rx.ip.pshdr_sum) or self._packet_sanity_check() - - if not packet_rx.parse_failed: - packet_rx.hptr = self._hptr + UDP_HEADER_LEN - - def __str__(self): - """Packet log string""" - - return f"UDP {self.sport} > {self.dport}, len {self.plen}" - - def __len__(self): - """Number of bytes remaining in the frame""" - - return len(self._frame) - self._hptr - - @property - def sport(self): - """Read 'Source port' field""" - - if self.__sport is self.__not_cached: - self.__sport = struct.unpack_from("!H", self._frame, self._hptr + 0)[0] - return self.__sport - - @property - def dport(self): - """Read 'Destianation port' field""" - - if self.__dport is self.__not_cached: - self.__dport = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] - return self.__dport - - @property - def plen(self): - """Read 'Packet length' field""" - - if self.__plen is self.__not_cached: - self.__plen = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] - return self.__plen - - @property - def cksum(self): - """Read 'Checksum' field""" - - if self.__cksum is self.__not_cached: - self.__cksum = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] - return self.__cksum - - @property - def data(self): - """Read the data packet carries""" - - if self.__data is self.__not_cached: - self.__data = self._frame[self._hptr + UDP_HEADER_LEN : self._hptr + self.plen] - return self.__data - - @property - def dlen(self): - """Calculate data length""" - - return self.plen - UDP_HEADER_LEN - - @property - def packet(self): - """Read the whole packet""" - - if self.__packet is self.__not_cached: - self.__packet = self._frame[self._hptr :] - return self.__packet - - def _packet_integrity_check(self, pshdr_sum): - """Packet integrity check to be run on raw frame prior to parsing to make sure parsing is safe""" - - if not config.packet_integrity_check: - return False - - if inet_cksum(self._frame, self._hptr, self._plen, pshdr_sum): - return "UDP integrity - wrong packet checksum" - - if not UDP_HEADER_LEN <= self._plen <= len(self): - return "UDP integrity - wrong packet length (I)" - - plen = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] - if not UDP_HEADER_LEN <= plen == self._plen <= len(self): - return "UDP integrity - wrong packet length (II)" - - return False - - def _packet_sanity_check(self): - """Packet sanity check to be run on parsed packet to make sure frame's fields contain sane values""" - - if not config.packet_sanity_check: - return False - - if self.sport == 0: - return "UDP sanity - 'udp_sport' must be greater than 0" - - if self.dport == 0: - return "UDP sanity - 'udp_dport' must be greater then 0" - - return False diff --git a/icmp4/__init__.py b/icmp4/__init__.py new file mode 100755 index 00000000..43855e5f --- /dev/null +++ b/icmp4/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# icmp4/__init__.py +# diff --git a/icmp4/fpa.py b/icmp4/fpa.py new file mode 100755 index 00000000..250e9387 --- /dev/null +++ b/icmp4/fpa.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# icmp4/fpa.py - Fast Packet Assembler support class for ICMPv4 protocol +# + + +import struct +from typing import Optional + +import icmp4.ps +import ip4.ps +from misc.ip_helper import inet_cksum +from misc.tracker import Tracker + + +class Assembler: + """ICMPv4 packet assembler support class""" + + ip4_proto = ip4.ps.PROTO_ICMP4 + + def __init__( + self, + type: int, + code: int = 0, + ec_id: Optional[int] = None, + ec_seq: Optional[int] = None, + ec_data: Optional[bytes] = None, + un_data: Optional[bytes] = None, + echo_tracker: Optional[Tracker] = None, + ) -> None: + """Class constructor""" + + assert type in {icmp4.ps.ECHO_REQUEST, icmp4.ps.UNREACHABLE, icmp4.ps.ECHO_REPLY} + + self.tracker = Tracker("TX", echo_tracker) + self.type = type + self.code = code + + if self.type == icmp4.ps.ECHO_REPLY: + self.ec_id = ec_id + self.ec_seq = ec_seq + self.ec_data = b"" if ec_data is None else ec_data + + elif self.type == icmp4.ps.UNREACHABLE and self.code == icmp4.ps.UNREACHABLE__PORT: + self.un_data = b"" if un_data is None else un_data[:520] + + elif self.type == icmp4.ps.ECHO_REQUEST: + self.ec_id = ec_id + self.ec_seq = ec_seq + self.ec_data = b"" if ec_data is None else ec_data + + def __len__(self) -> int: + """Length of the packet""" + + if self.type == icmp4.ps.ECHO_REPLY: + return icmp4.ps.ECHO_REPLY_LEN + len(self.ec_data) + + if self.type == icmp4.ps.UNREACHABLE and self.code == icmp4.ps.UNREACHABLE__PORT: + return icmp4.ps.UNREACHABLE_LEN + len(self.un_data) + + if self.type == icmp4.ps.ECHO_REQUEST: + return icmp4.ps.ECHO_REQUEST_LEN + len(self.ec_data) + + return 0 + + from icmp4.ps import __str__ + + def assemble(self, frame: bytearray, hptr: int, _: int) -> None: + """Assemble packet into the raw form""" + + if self.type == icmp4.ps.ECHO_REPLY: + struct.pack_into(f"! BBH HH {len(self.ec_data)}s", frame, hptr, self.type, self.code, 0, self.ec_id, self.ec_seq, self.ec_data) + + elif self.type == icmp4.ps.UNREACHABLE and self.code == icmp4.ps.UNREACHABLE__PORT: + struct.pack_into(f"! BBH L {len(self.un_data)}s", frame, hptr, self.type, self.code, 0, 0, self.un_data) + + elif self.type == icmp4.ps.ECHO_REQUEST: + struct.pack_into(f"! BBH HH {len(self.ec_data)}s", frame, hptr, self.type, self.code, 0, self.ec_id, self.ec_seq, self.ec_data) + + struct.pack_into("! H", frame, hptr + 2, inet_cksum(frame, hptr, len(self))) diff --git a/icmp4/fpp.py b/icmp4/fpp.py new file mode 100755 index 00000000..7b57322b --- /dev/null +++ b/icmp4/fpp.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# icmp4/fpp.py - Fast Packet Parser support class for ICMPv4 protocol +# + + +import struct +from typing import cast + +import config +import icmp4.ps +from ip4.fpp import Parser as Ip4Parser +from misc.ip_helper import inet_cksum +from misc.packet import PacketRx + + +class Parser: + """ICMPv4 packet parser class""" + + def __init__(self, packet_rx: PacketRx) -> None: + """Class constructor""" + + packet_rx.icmp4 = self + + self._frame = packet_rx.frame + self._hptr = packet_rx.hptr + packet_rx.ip4 = cast(Ip4Parser, packet_rx.ip4) + self._plen = packet_rx.ip4.dlen + + packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() + + def __len__(self) -> int: + """Number of bytes remaining in the frame""" + + return len(self._frame) - self._hptr + + from icmp4.ps import __str__ + + @property + def type(self) -> int: + """Read 'Type' field""" + + return self._frame[self._hptr + 0] + + @property + def code(self) -> int: + """Read 'Code' field""" + + return self._frame[self._hptr + 1] + + @property + def cksum(self) -> int: + """Read 'Checksum' field""" + + if "_cache__cksum" not in self.__dict__: + self._cache__cksum = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] + return self._cache__cksum + + @property + def ec_id(self) -> int: + """Read Echo 'Id' field""" + + if "_cache__ec_id" not in self.__dict__: + assert self.type in {icmp4.ps.ECHO_REQUEST, icmp4.ps.ECHO_REPLY} + self._cache__ec_id = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] + return self._cache__ec_id + + @property + def ec_seq(self) -> int: + """Read Echo 'Seq' field""" + + if "_cache__ec_seq" not in self.__dict__: + assert self.type in {icmp4.ps.ECHO_REQUEST, icmp4.ps.ECHO_REPLY} + self._cache__ec_seq = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] + return self._cache__ec_seq + + @property + def ec_data(self) -> bytes: + """Read data carried by Echo message""" + + if "_cache__ec_data" not in self.__dict__: + assert self.type in {icmp4.ps.ECHO_REQUEST, icmp4.ps.ECHO_REPLY} + self._cache__ec_data = self._frame[self._hptr + 8 : self._hptr + self.plen] + return self._cache__ec_data + + @property + def un_data(self) -> bytes: + """Read data carried by Uneachable message""" + + if "_cache__un_data" not in self.__dict__: + assert self.type == icmp4.ps.UNREACHABLE + self._cache__un_data = self._frame[self._hptr + 8 : self._hptr + self.plen] + return self._cache__un_data + + @property + def plen(self) -> int: + """Calculate packet length""" + + return self._plen + + @property + def packet_copy(self) -> bytes: + """Read the whole packet""" + + if "_cache__packet_copy" not in self.__dict__: + self._cache__packet_copy = self._frame[self._hptr : self._hptr + self.plen] + return self._cache__packet_copy + + def _packet_integrity_check(self) -> str: + """Packet integrity check to be run on raw frame prior to parsing to make sure parsing is safe""" + + if not config.packet_integrity_check: + return "" + + if inet_cksum(self._frame, self._hptr, self._plen): + return "ICMPv4 integrity - wrong packet checksum" + + if not icmp4.ps.HEADER_LEN <= self._plen <= len(self): + return "ICMPv4 integrity - wrong packet length (I)" + + if self._frame[self._hptr + 0] in {icmp4.ps.ECHO_REQUEST, icmp4.ps.ECHO_REPLY}: + if not 8 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + + elif self._frame[self._hptr + 0] == icmp4.ps.UNREACHABLE: + if not 12 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + + return "" + + def _packet_sanity_check(self) -> str: + """Packet sanity check to be run on parsed packet to make sure frame's fields contain sane values""" + + if not config.packet_sanity_check: + return "" + + if self.type in {icmp4.ps.ECHO_REQUEST, icmp4.ps.ECHO_REPLY}: + if not self.code == 0: + return "ICMPv4 sanity - 'code' should be set to 0 (RFC 792)" + + if self.type == icmp4.ps.UNREACHABLE: + if self.code not in {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}: + return "ICMPv4 sanity - 'code' must be set to [0-15] (RFC 792)" + + return "" diff --git a/phrx_icmp4.py b/icmp4/phrx.py similarity index 66% rename from phrx_icmp4.py rename to icmp4/phrx.py index 87ea560d..c384fc35 100755 --- a/phrx_icmp4.py +++ b/icmp4/phrx.py @@ -23,31 +23,25 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phrx_icmp4.py - packet handler for inbound ICMPv4 packets +# icmp4/phrx.py - packet handler for inbound ICMPv4 packets # -import fpp_icmp4 +from typing import cast + +import icmp4.fpp +import icmp4.ps +from icmp4.fpp import Parser as Icmp4Parser +from ip4.fpp import Parser as Ip4Parser +from misc.packet import PacketRx -def _phrx_icmp4(self, packet_rx): +def _phrx_icmp4(self, packet_rx: PacketRx) -> None: """Handle inbound ICMPv4 packets""" - fpp_icmp4.Icmp4Packet(packet_rx) + icmp4.fpp.Parser(packet_rx) if packet_rx.parse_failed: if __debug__: @@ -57,15 +51,18 @@ def _phrx_icmp4(self, packet_rx): if __debug__: self._logger.opt(ansi=True).info(f"{packet_rx.tracker} - {packet_rx.icmp4}") + packet_rx.icmp4 = cast(Icmp4Parser, packet_rx.icmp4) + # Respond to ICMPv4 Echo Request packet - if packet_rx.icmp4.type == fpp_icmp4.ICMP4_ECHO_REQUEST: + packet_rx.ip4 = cast(Ip4Parser, packet_rx.ip4) + if packet_rx.icmp4.type == icmp4.ps.ECHO_REQUEST: if __debug__: self._logger.debug(f"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=fpp_icmp4.ICMP4_ECHO_REPLY, + icmp4_type=icmp4.ps.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, diff --git a/phtx_icmp4.py b/icmp4/phtx.py similarity index 61% rename from phtx_icmp4.py rename to icmp4/phtx.py index 0def12c6..39daa794 100755 --- a/phtx_icmp4.py +++ b/icmp4/phtx.py @@ -23,47 +23,40 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phtx_icmp4.py - packet handler for outbound ICMPv4 packets +# icmp4/phtx.py - packet handler for outbound ICMPv4 packets # +from typing import Optional + import config -import fpa_icmp4 +import icmp4.fpa +from misc.ipv4_address import IPv4Address +from misc.ipv6_address import IPv6Address +from misc.tracker import Tracker def _phtx_icmp4( self, - ip4_src, - ip4_dst, - icmp4_type, - icmp4_code=0, - icmp4_ec_id=None, - icmp4_ec_seq=None, - icmp4_ec_data=None, - icmp4_un_data=None, - echo_tracker=None, -): + ip4_src: IPv4Address, + ip4_dst: IPv6Address, + icmp4_type: int, + icmp4_code: int = 0, + icmp4_ec_id: Optional[int] = None, + icmp4_ec_seq: Optional[int] = None, + icmp4_ec_data: Optional[bytes] = None, + icmp4_un_data: Optional[bytes] = None, + echo_tracker: Optional[Tracker] = None, +) -> None: """Handle outbound ICMPv4 packets""" # Check if IPv4 protocol support is enabled, if not then silently drop the packet if not config.ip4_support: return - icmp4_packet_tx = fpa_icmp4.Icmp4Packet( + icmp4_packet_tx = icmp4.fpa.Assembler( type=icmp4_type, code=icmp4_code, ec_id=icmp4_ec_id, @@ -75,4 +68,4 @@ def _phtx_icmp4( if __debug__: self._logger.opt(ansi=True).info(f"{icmp4_packet_tx.tracker} - {icmp4_packet_tx}") - self._phtx_ip4(ip4_src=ip4_src, ip4_dst=ip4_dst, child_packet=icmp4_packet_tx) + self._phtx_ip4(ip4_src=ip4_src, ip4_dst=ip4_dst, carried_packet=icmp4_packet_tx) diff --git a/icmp4/ps.py b/icmp4/ps.py new file mode 100755 index 00000000..bc9f7020 --- /dev/null +++ b/icmp4/ps.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# icmp4/ps.py - protocol support for ICMPv4 +# + + +# Echo reply message (0/0) + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Id | Seq | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# ~ Data ~ +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +# Destination Unreachable message (3/[0-3, 5-15]) + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Reserved | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# ~ Data ~ +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +# Destination Unreachable message (3/4) + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Reserved | Link MTU / 0 | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# ~ Data ~ +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +# Echo Request message (8/0) + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Type | Code | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Id | Seq | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# ~ Data ~ +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + +HEADER_LEN = 4 + +ECHO_REPLY = 0 +ECHO_REPLY_LEN = 8 +UNREACHABLE = 3 +UNREACHABLE_LEN = 8 +UNREACHABLE__NET = 0 +UNREACHABLE__HOST = 1 +UNREACHABLE__PROTOCOL = 2 +UNREACHABLE__PORT = 3 +UNREACHABLE__FAGMENTATION = 4 +UNREACHABLE__SOURCE_ROUTE_FAILED = 5 +ECHO_REQUEST = 8 +ECHO_REQUEST_LEN = 8 + + +def __str__(self) -> str: + """Packet log string""" + + log = f"ICMPv4 type {self.type}, code {self.code}" + + if self.type == ECHO_REPLY: + log += f", id {self.ec_id}, seq {self.ec_seq}" + + elif self.type == UNREACHABLE and self.code == UNREACHABLE__PORT: + pass + + elif self.type == ECHO_REQUEST: + log += f", id {self.ec_id}, seq {self.ec_seq}" + + return log diff --git a/icmp6/__init__.py b/icmp6/__init__.py new file mode 100755 index 00000000..06b16d33 --- /dev/null +++ b/icmp6/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# icmp6/__init__.py +# diff --git a/icmp6/fpa.py b/icmp6/fpa.py new file mode 100755 index 00000000..7db2f290 --- /dev/null +++ b/icmp6/fpa.py @@ -0,0 +1,354 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# icmp6/fpa.py - Fast Packet Assembler support class for ICMPv6 protocol +# + + +import struct +from typing import Optional + +import icmp6.ps +import ip6.ps +from misc.ip_helper import inet_cksum +from misc.ipv6_address import IPv6Address, IPv6Network +from misc.tracker import Tracker + + +class Assembler: + """ICMPv6 packet assembler support class""" + + ip6_next = ip6.ps.NEXT_HEADER_ICMP6 + + def __init__( + self, + type: int, + code: int = 0, + un_data: Optional[bytes] = None, + ec_id: Optional[int] = None, + ec_seq: Optional[int] = None, + ec_data: Optional[bytes] = None, + ra_hop: Optional[int] = None, + ra_flag_m: Optional[bool] = None, + ra_flag_o: Optional[bool] = None, + ra_router_lifetime: Optional[int] = None, + ra_reachable_time: Optional[int] = None, + ra_retrans_timer: Optional[int] = None, + ns_target_address: Optional[IPv6Address] = None, + na_flag_r: Optional[bool] = None, + na_flag_s: Optional[bool] = None, + na_flag_o: Optional[bool] = None, + na_target_address: Optional[IPv6Address] = None, + nd_options: Optional[list] = None, + mlr2_multicast_address_record: Optional[list] = None, + echo_tracker: Optional[Tracker] = None, + ) -> None: + """Class constructor""" + + self.tracker = Tracker("TX", echo_tracker) + + self.type = type + self.code = code + + self.nd_options = [] if nd_options is None else nd_options + + if self.type == icmp6.ps.UNREACHABLE: + self.un_reserved = 0 + self.un_data = b"" if un_data is None else un_data[:520] + + elif self.type == icmp6.ps.ECHO_REQUEST: + self.ec_id = ec_id + self.ec_seq = ec_seq + self.ec_data = b"" if ec_data is None else ec_data + + elif self.type == icmp6.ps.ECHO_REPLY: + self.ec_id = ec_id + self.ec_seq = ec_seq + self.ec_data = b"" if ec_data is None else ec_data + + elif self.type == icmp6.ps.ROUTER_SOLICITATION: + self.rs_reserved = 0 + + elif self.type == icmp6.ps.ROUTER_ADVERTISEMENT: + self.ra_hop = ra_hop + self.ra_flag_m = False if ra_flag_m is None else ra_flag_m + self.ra_flag_o = False if ra_flag_o is None else ra_flag_o + self.ra_router_lifetime = ra_router_lifetime + self.ra_reachable_time = ra_reachable_time + self.ra_retrans_timer = ra_retrans_timer + + elif self.type == icmp6.ps.NEIGHBOR_SOLICITATION: + self.ns_reserved = 0 + self.ns_target_address = ns_target_address + + elif self.type == icmp6.ps.NEIGHBOR_ADVERTISEMENT: + self.na_flag_r = False if na_flag_r is None else na_flag_r + self.na_flag_s = False if na_flag_s is None else na_flag_s + self.na_flag_o = False if na_flag_o is None else na_flag_o + self.na_reserved = 0 + self.na_target_address = na_target_address + + elif self.type == icmp6.ps.MLD2_REPORT: + self.mlr2_reserved = 0 + self.mlr2_multicast_address_record = [] if mlr2_multicast_address_record is None else mlr2_multicast_address_record + self.mlr2_number_of_multicast_address_records = len(self.mlr2_multicast_address_record) + + def __len__(self) -> int: + """Length of the packet""" + + if self.type == icmp6.ps.UNREACHABLE: + return icmp6.ps.UNREACHABLE_LEN + len(self.un_data) + + if self.type == icmp6.ps.ECHO_REQUEST: + return icmp6.ps.ECHO_REQUEST_LEN + len(self.ec_data) + + if self.type == icmp6.ps.ECHO_REPLY: + return icmp6.ps.ECHO_REPLY_LEN + len(self.ec_data) + + if self.type == icmp6.ps.ROUTER_SOLICITATION: + assert self.nd_options is not None + return icmp6.ps.ROUTER_SOLICITATION_LEN + sum([len(_) for _ in self.nd_options]) + + if self.type == icmp6.ps.ROUTER_ADVERTISEMENT: + assert self.nd_options is not None + return icmp6.ps.ROUTER_ADVERTISEMENT_LEN + sum([len(_) for _ in self.nd_options]) + + if self.type == icmp6.ps.NEIGHBOR_SOLICITATION: + assert self.nd_options is not None + return icmp6.ps.NEIGHBOR_SOLICITATION_LEN + sum([len(_) for _ in self.nd_options]) + + if self.type == icmp6.ps.NEIGHBOR_ADVERTISEMENT: + assert self.nd_options is not None + return icmp6.ps.NEIGHBOR_ADVERTISEMENT_LEN + sum([len(_) for _ in self.nd_options]) + + if self.type == icmp6.ps.MLD2_REPORT: + return icmp6.ps.MLD2_REPORT_LEN + sum([len(_) for _ in self.mlr2_multicast_address_record]) + + return 0 + + from icmp6.ps import __str__ + + def assemble(self, frame: bytearray, hptr: int, pshdr_sum: int) -> None: + """Assemble packet into the raw form""" + + if self.type == icmp6.ps.UNREACHABLE: + struct.pack_into(f"! BBH L {len(self.un_data)}s", frame, hptr, self.type, self.code, 0, self.un_reserved, self.un_data) + + elif self.type == icmp6.ps.ECHO_REQUEST: + struct.pack_into(f"! BBH HH {len(self.ec_data)}s", frame, hptr, self.type, self.code, 0, self.ec_id, self.ec_seq, self.ec_data) + + elif self.type == icmp6.ps.ECHO_REPLY: + struct.pack_into(f"! BBH HH {len(self.ec_data)}s", frame, hptr, self.type, self.code, 0, self.ec_id, self.ec_seq, self.ec_data) + + elif self.type == icmp6.ps.ROUTER_SOLICITATION: + struct.pack_into(f"! BBH L {len(self.raw_nd_options)}s", frame, hptr, self.type, self.code, 0, self.rs_reserved, self.raw_nd_options) + + elif self.type == icmp6.ps.ROUTER_ADVERTISEMENT: + assert self.ra_flag_m is not None + assert self.ra_flag_o is not None + struct.pack_into( + f"! BBH BBH L L {len(self.raw_nd_options)}s", + frame, + hptr, + self.type, + self.code, + 0, + self.ra_hop, + (self.ra_flag_m << 7) | (self.ra_flag_o << 6), + self.ra_router_lifetime, + self.ra_reachable_time, + self.ra_retrans_timer, + self.raw_nd_options, + ) + + elif self.type == icmp6.ps.NEIGHBOR_SOLICITATION: + assert self.ns_target_address is not None + struct.pack_into( + f"! BBH L 16s {len(self.raw_nd_options)}s", + frame, + hptr, + self.type, + self.code, + 0, + self.ns_reserved, + self.ns_target_address.packed, + self.raw_nd_options, + ) + + elif self.type == icmp6.ps.NEIGHBOR_ADVERTISEMENT: + assert self.na_flag_r is not None + assert self.na_flag_s is not None + assert self.na_flag_o is not None + assert self.na_target_address is not None + struct.pack_into( + f"! BBH L 16s {len(self.raw_nd_options)}s", + frame, + hptr, + self.type, + self.code, + 0, + (self.na_flag_r << 31) | (self.na_flag_s << 30) | (self.na_flag_o << 29) | self.na_reserved, + self.na_target_address.packed, + self.raw_nd_options, + ) + + elif self.type == icmp6.ps.MLD2_REPORT: + struct.pack_into( + f"! BBH HH {sum([len(_) for _ in self.mlr2_multicast_address_record])}s", + frame, + hptr, + self.type, + self.code, + 0, + self.mlr2_reserved, + self.mlr2_number_of_multicast_address_records, + b"".join([_.raw_record for _ in self.mlr2_multicast_address_record]), + ) + + struct.pack_into("! H", frame, hptr + 2, inet_cksum(frame, hptr, len(self), pshdr_sum)) + + @property + def raw_nd_options(self) -> bytes: + """ICMPv6 ND packet options in raw format""" + + assert self.nd_options is not None + + raw_nd_options = b"" + + for option in self.nd_options: + raw_nd_options += option.raw_option + + return raw_nd_options + + +# +# ICMPv6 Neighbor Discovery options +# + + +class NdOptSLLA(icmp6.ps.NdOptSLLA): + """ICMPv6 ND option - Source Link Layer Address (1)""" + + def __init__(self, slla: str) -> None: + self.slla = slla + + @property + def raw_option(self) -> bytes: + return struct.pack("! BB 6s", icmp6.ps.ND_OPT_SLLA, icmp6.ps.ND_OPT_SLLA_LEN >> 3, bytes.fromhex(self.slla.replace(":", ""))) + + +class NdOptTLLA(icmp6.ps.NdOptTLLA): + """ICMPv6 ND option - Target Link Layer Address (2)""" + + def __init__(self, tlla: str) -> None: + self.tlla = tlla + + @property + def raw_option(self) -> bytes: + return struct.pack("! BB 6s", icmp6.ps.ND_OPT_TLLA, icmp6.ps.ND_OPT_TLLA_LEN >> 3, bytes.fromhex(self.tlla.replace(":", ""))) + + +class NdOptPI(icmp6.ps.NdOptPI): + """ICMPv6 ND option - Prefix Information (3)""" + + def __init__( + self, + valid_lifetime: int, + preferred_lifetime: int, + prefix: IPv6Network, + flag_l: bool = False, + flag_a: bool = False, + flag_r: bool = False, + ) -> None: + self.code = icmp6.ps.ND_OPT_PI + self.len = icmp6.ps.ND_OPT_PI_LEN + self.flag_l = flag_l + self.flag_a = flag_a + self.flag_r = flag_r + self.valid_lifetime = valid_lifetime + self.preferred_lifetime = preferred_lifetime + self.prefix = IPv6Network(prefix) + + @property + def raw_option(self) -> bytes: + return struct.pack( + "! BB BB L L L 16s", + self.code, + self.len >> 3, + self.prefix.prefixlen, + (self.flag_l << 7) | (self.flag_a << 6) | (self.flag_r << 6), + self.valid_lifetime, + self.preferred_lifetime, + self.prefix.network_address.packed, + ) + + +# +# ICMPv6 Multicast support classes +# + + +class MulticastAddressRecord: + """Multicast Address Record used by MLDv2 Report message""" + + def __init__(self, record_type: int, multicast_address: IPv6Address, source_address: Optional[list] = None, aux_data: Optional[bytes] = None): + """Class constructor""" + + self.record_type = record_type + self.multicast_address = IPv6Address(multicast_address) + self.source_address = [] if source_address is None else source_address + self.number_of_sources = len(self.source_address) + self.aux_data = b"" if aux_data is None else aux_data + self.aux_data_len = len(self.aux_data) + + def __len__(self) -> int: + """Length of raw record""" + + return len(self.raw_record) + + def __hash__(self) -> int: + """Hash of raw record""" + + return hash(self.raw_record) + + def __eq__(self, other: object) -> bool: + """Compare two records""" + + if not isinstance(other, MulticastAddressRecord): + return NotImplemented + + return self.raw_record == other.raw_record + + @property + def raw_record(self) -> bytes: + """Get record in raw format""" + + return ( + struct.pack("! BBH 16s", self.record_type, self.aux_data_len, self.number_of_sources, self.multicast_address.packed) + + b"".join([_.packed for _ in self.source_address]) + + self.aux_data + ) diff --git a/icmp6/fpp.py b/icmp6/fpp.py new file mode 100755 index 00000000..b38107ee --- /dev/null +++ b/icmp6/fpp.py @@ -0,0 +1,575 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# icmp6/fpp.py - Fast Packet Parse support class for ICMPv6 protocol +# + + +import struct +from typing import Optional, cast + +import config +import icmp6.ps +from ip6.fpp import Parser as Ip6Parser +from misc.ip_helper import inet_cksum +from misc.ipv6_address import IPv6Address, IPv6Network +from misc.packet import PacketRx + + +class Parser: + """ICMPv6 packet parser class""" + + def __init__(self, packet_rx: PacketRx) -> None: + """Class constructor""" + + packet_rx.icmp6 = self + + self._frame = packet_rx.frame + self._hptr = packet_rx.hptr + packet_rx.ip6 = cast(Ip6Parser, packet_rx.ip6) + self._plen = packet_rx.ip6.dlen + + packet_rx.parse_failed = self._packet_integrity_check(packet_rx.ip6.pshdr_sum) or self._packet_sanity_check( + packet_rx.ip6.src, packet_rx.ip6.dst, packet_rx.ip6.hop + ) + + def __len__(self) -> int: + """Number of bytes remaining in the frame""" + + return len(self._frame) - self._hptr + + from icmp6.ps import __str__ + + @property + def type(self) -> int: + """Read 'Type' field""" + + return self._frame[self._hptr + 0] + + @property + def code(self) -> int: + """Read 'Code' field""" + + return self._frame[self._hptr + 1] + + @property + def cksum(self) -> int: + """Read 'Checksum' field""" + + if "_cache__cksum" not in self.__dict__: + self._cache__cksum = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] + return self._cache__cksum + + @property + def un_data(self) -> bytes: + """Read data carried by Unreachable message""" + + if "_cache__un_data" not in self.__dict__: + assert self.type == icmp6.ps.UNREACHABLE + self._cache__un_data = self._frame[self._hptr + 8 : self._hptr + self.plen] + return self._cache__un_data + + @property + def ec_id(self) -> int: + """Read Echo 'Id' field""" + + if "_cache__ec_id" not in self.__dict__: + assert self.type in {icmp6.ps.ECHO_REQUEST, icmp6.ps.ECHO_REPLY} + self._cache__ec_id = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] + return self._cache__ec_id + + @property + def ec_seq(self) -> int: + """Read Echo 'Seq' field""" + + if "_cache__ec_seq" not in self.__dict__: + assert self.type in {icmp6.ps.ECHO_REQUEST, icmp6.ps.ECHO_REPLY} + self._cache__ec_seq = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] + return self._cache__ec_seq + + @property + def ec_data(self) -> bytes: + """Read data carried by Echo message""" + + if "_cache__ec_data" not in self.__dict__: + assert self.type in {icmp6.ps.ECHO_REQUEST, icmp6.ps.ECHO_REPLY} + self._cache__ec_data = self._frame[self._hptr + 8 : self._hptr + self.plen] + return self._cache__ec_data + + @property + def ra_hop(self) -> int: + """Read ND RA 'Hop limit' field""" + + assert self.type == icmp6.ps.ROUTER_ADVERTISEMENT + return self._frame[self._hptr + 4] + + @property + def ra_flag_m(self) -> bool: + """Read ND RA 'M flag' field""" + + if "_cache__ra_flag_m" not in self.__dict__: + assert self.type == icmp6.ps.ROUTER_ADVERTISEMENT + self._cache__ra_flag_m = bool(self._frame[self._hptr + 5] & 0b10000000) + return self._cache__ra_flag_m + + @property + def ra_flag_o(self) -> bool: + """Read ND RA 'O flag' field""" + + if "_cache__ra_flag_o" not in self.__dict__: + assert self.type == icmp6.ps.ROUTER_ADVERTISEMENT + self._cache__ra_flag_o = bool(self._frame[self._hptr + 5] & 0b01000000) + return self._cache__ra_flag_o + + @property + def ra_router_lifetime(self) -> int: + """Read ND RA 'Router lifetime' field""" + + if "_cache__ra_router_lifetime" not in self.__dict__: + assert self.type == icmp6.ps.ROUTER_ADVERTISEMENT + self._cache__ra_router_lifetime = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] + return self._cache__ra_router_lifetime + + @property + def ra_reachable_time(self) -> int: + """Read ND RA 'Reachable time' field""" + + if "_cache__ra_reachable_time" not in self.__dict__: + assert self.type == icmp6.ps.ROUTER_ADVERTISEMENT + self._cache__ra_reachable_time = struct.unpack_from("!L", self._frame, self._hptr + 8)[0] + return self._cache__ra_reachable_time + + @property + def ra_retrans_timer(self) -> int: + """Read ND RA 'Retransmision timer' field""" + + if "_cache__ra_retrans_timer" not in self.__dict__: + assert self.type == icmp6.ps.ROUTER_ADVERTISEMENT + self._cache__ra_retrans_timer = struct.unpack_from("!L", self._frame, self._hptr + 12)[0] + return self._cache__ra_retrans_timer + + @property + def ns_target_address(self) -> IPv6Address: + """Read ND NS 'Target adress' field""" + + if "_cache__ns_target_address" not in self.__dict__: + assert self.type == icmp6.ps.NEIGHBOR_SOLICITATION + self._cache__ns_target_address = IPv6Address(self._frame[self._hptr + 8 : self._hptr + 24]) + return self._cache__ns_target_address + + @property + def na_flag_r(self) -> bool: + """Read ND NA 'R flag' field""" + + if "_cache__na_flag_r" not in self.__dict__: + assert self.type == icmp6.ps.NEIGHBOR_ADVERTISEMENT + self._cache__na_flag_r = bool(self._frame[self._hptr + 4] & 0b10000000) + return self._cache__na_flag_r + + @property + def na_flag_s(self) -> bool: + """Read ND NA 'S flag' field""" + + if "_cache__na_flag_s" not in self.__dict__: + assert self.type == icmp6.ps.NEIGHBOR_ADVERTISEMENT + self._cache__na_flag_s = bool(self._frame[self._hptr + 4] & 0b01000000) + return self._cache__na_flag_s + + @property + def na_flag_o(self) -> bool: + """Read ND NA 'O flag' field""" + + if "_cache__na_flag_o" not in self.__dict__: + assert self.type == icmp6.ps.NEIGHBOR_ADVERTISEMENT + self._cache__na_flag_o = bool(self._frame[self._hptr + 4] & 0b00100000) + return self._cache__na_flag_o + + @property + def na_target_address(self) -> IPv6Address: + """Read ND NA 'Taret address' field""" + + if "_cache__na_target_address" not in self.__dict__: + assert self.type == icmp6.ps.NEIGHBOR_ADVERTISEMENT + self._cache__na_target_address = IPv6Address(self._frame[self._hptr + 8 : self._hptr + 24]) + return self._cache__na_target_address + + @property + def mld2_rep_nor(self) -> int: + """Read MLD2 Report 'Number of multicast address records' field""" + + if "_cache__mld2_rep_nor" not in self.__dict__: + assert self.type == icmp6.ps.MLD2_REPORT + self._cache__mld2_rep_nor = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] + return self._cache__mld2_rep_nor + + @property + def mld2_rep_records(self) -> list: + """Read MLD2 Report record list""" + + if "_cache__mld2_rep_records" not in self.__dict__: + assert self.type == icmp6.ps.MLD2_REPORT + self._cache__mld2_rep_records = [] + raw_records = self._frame[self._hptr + 8 :] + for _ in range(self.mld2_rep_nor): + record = MulticastAddressRecord(raw_records) + raw_records = raw_records[len(record) :] + self._cache__mld2_rep_records.append(record) + return self._cache__mld2_rep_records + + def _read_nd_options(self, optr: int) -> list: + """Read ND options""" + + nd_options = [] + while optr < len(self._frame): + nd_options.append( + {icmp6.ps.ND_OPT_SLLA: NdOptSLLA, icmp6.ps.ND_OPT_TLLA: NdOptTLLA, icmp6.ps.ND_OPT_PI: NdOptPI}.get(self._frame[optr], NdOptUnk)( + self._frame, optr + ) + ) + optr += self._frame[optr + 1] << 3 + + return nd_options + + @property + def nd_options(self) -> list: + """Read ND options""" + + if "_cache__nd_options" not in self.__dict__: + assert self.type in { + icmp6.ps.ROUTER_SOLICITATION, + icmp6.ps.ROUTER_ADVERTISEMENT, + icmp6.ps.NEIGHBOR_SOLICITATION, + icmp6.ps.NEIGHBOR_ADVERTISEMENT, + } + optr = self._hptr + { + icmp6.ps.ROUTER_SOLICITATION: 12, + icmp6.ps.ROUTER_ADVERTISEMENT: 16, + icmp6.ps.NEIGHBOR_SOLICITATION: 24, + icmp6.ps.NEIGHBOR_ADVERTISEMENT: 24, + }[self.type] + self._cache__nd_options = self._read_nd_options(optr) + return self._cache__nd_options + + @property + def nd_opt_slla(self) -> Optional[str]: + """ICMPv6 ND option - Source Link Layer Address (1)""" + + if "_cache__nd_opt_slla" not in self.__dict__: + assert self.type in {icmp6.ps.ROUTER_SOLICITATION, icmp6.ps.ROUTER_ADVERTISEMENT, icmp6.ps.NEIGHBOR_SOLICITATION, icmp6.ps.NEIGHBOR_ADVERTISEMENT} + for option in self.nd_options: + if option.code == icmp6.ps.ND_OPT_SLLA: + self._cache__nd_opt_slla = option.slla + break + else: + self._cache__nd_opt_slla = None + return self._cache__nd_opt_slla + + @property + def nd_opt_tlla(self) -> Optional[str]: + """ICMPv6 ND option - Target Link Layer Address (2)""" + + if "_cache__nd_opt_tlla" not in self.__dict__: + assert self.type in {icmp6.ps.ROUTER_SOLICITATION, icmp6.ps.ROUTER_ADVERTISEMENT, icmp6.ps.NEIGHBOR_SOLICITATION, icmp6.ps.NEIGHBOR_ADVERTISEMENT} + for option in self.nd_options: + if option.code == icmp6.ps.ND_OPT_TLLA: + self._cache__nd_opt_tlla = option.tlla + break + else: + self._cache__nd_opt_tlla = None + return self._cache__nd_opt_tlla + + @property + def nd_opt_pi(self) -> list: + """ICMPv6 ND option - Prefix Info (3) - Returns list of prefixes that can be used for address autoconfiguration""" + + if "_cache__nd_opt_pi" not in self.__dict__: + assert self.type in {icmp6.ps.ROUTER_SOLICITATION, icmp6.ps.ROUTER_ADVERTISEMENT, icmp6.ps.NEIGHBOR_SOLICITATION, icmp6.ps.NEIGHBOR_ADVERTISEMENT} + self._cache__nd_opt_pi = [_.prefix for _ in self.nd_options if _.code == icmp6.ps.ND_OPT_PI and _.flag_a and _.prefix.prefixlen == 64] + return self._cache__nd_opt_pi + + @property + def plen(self) -> int: + """Calculate packet length""" + + return self._plen + + @property + def packet_copy(self) -> bytes: + """Read the whole packet""" + + if "_cache__packet_copy" not in self.__dict__: + self._cache__packet_copy = self._frame[self._hptr : self._hptr + self.plen] + return self._cache__packet_copy + + def _nd_option_integrity_check(self, optr: int) -> str: + """Check integrity of ICMPv6 ND options""" + + while optr < len(self._frame): + if optr + 1 > len(self._frame): + return "ICMPv6 sanity check fail - wrong option length (I)" + if self._frame[optr + 1] == 0: + return "ICMPv6 sanity check fail - wrong option length (II)" + optr += self._frame[optr + 1] << 3 + if optr > len(self._frame): + return "ICMPv6 sanity check fail - wrong option length (III)" + + return "" + + def _packet_integrity_check(self, pshdr_sum: int) -> str: + """Packet integrity check to be run on raw frame prior to parsing to make sure parsing is safe""" + + if not config.packet_integrity_check: + return "" + + if inet_cksum(self._frame, self._hptr, self._plen, pshdr_sum): + return "ICMPv6 integrity - wrong packet checksum" + + if not icmp6.ps.HEADER_LEN <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (I)" + + if self._frame[0] == icmp6.ps.UNREACHABLE: + if not 12 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + + elif self._frame[0] in {icmp6.ps.ECHO_REQUEST, icmp6.ps.ECHO_REPLY}: + if not 8 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + + elif self._frame[0] == icmp6.ps.MLD2_QUERY: + if not 28 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + if self._plen != 28 + struct.unpack_from("! H", self._frame, self._hptr + 26)[0] * 16: + return "ICMPv6 integrity - wrong packet length (III)" + + elif self._frame[0] == icmp6.ps.ROUTER_SOLICITATION: + if not 8 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + if fail := self._nd_option_integrity_check(self._hptr + 8): + return fail + + elif self._frame[0] == icmp6.ps.ROUTER_ADVERTISEMENT: + if not 16 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + if fail := self._nd_option_integrity_check(self._hptr + 16): + return fail + + elif self._frame[0] == icmp6.ps.NEIGHBOR_SOLICITATION: + if not 24 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + if fail := self._nd_option_integrity_check(self._hptr + 24): + return fail + + elif self._frame[0] == icmp6.ps.NEIGHBOR_ADVERTISEMENT: + if 24 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + if fail := self._nd_option_integrity_check(self._hptr + 24): + return fail + + elif self._frame[0] == icmp6.ps.MLD2_REPORT: + if not 8 <= self._plen <= len(self): + return "ICMPv6 integrity - wrong packet length (II)" + optr = self._hptr + 8 + for _ in range(struct.unpack_from("! H", self._frame, self._hptr + 6)[0]): + if optr + 20 > self._hptr + self._plen: + return "ICMPv6 integrity - wrong packet length (III)" + optr += 20 + self._frame[optr + 1] + struct.unpack_from("! H", self._frame, optr + 2)[0] * 16 + if optr != self._hptr + self._plen: + return "ICMPv6 integrity - wrong packet length (IV)" + + return "" + + def _packet_sanity_check(self, ip6_src: IPv6Address, ip6_dst: IPv6Address, ip6_hop: int) -> str: + """Packet sanity check to be run on parsed packet to make sure frame's fields contain sane values""" + + if not config.packet_sanity_check: + return "" + + if self.type == icmp6.ps.UNREACHABLE: + if self.code not in {0, 1, 2, 3, 4, 5, 6}: + return "ICMPv6 sanity - 'code' must be [0-6] (RFC 4861)" + + elif self.type == icmp6.ps.PACKET_TOO_BIG: + if not self.code == 0: + return "ICMPv6 sanity - 'code' should be 0 (RFC 4861)" + + elif self.type == icmp6.ps.TIME_EXCEEDED: + if self.code not in {0, 1}: + return "ICMPv6 sanity - 'code' must be [0-1] (RFC 4861)" + + elif self.type == icmp6.ps.PARAMETER_PROBLEM: + if self.code not in {0, 1, 2}: + return "ICMPv6 sanity - 'code' must be [0-2] (RFC 4861)" + + elif self.type in {icmp6.ps.ECHO_REQUEST, icmp6.ps.ECHO_REPLY}: + if not self.code == 0: + return "ICMPv6 sanity - 'code' should be 0 (RFC 4861)" + + elif self.type == icmp6.ps.MLD2_QUERY: + if not self.code == 0: + return "ICMPv6 sanity - 'code' must be 0 (RFC 3810)" + if not ip6_hop == 1: + return "ICMPv6 sanity - 'hop' must be 255 (RFC 3810)" + + elif self.type == icmp6.ps.ROUTER_SOLICITATION: + if not self.code == 0: + return "ICMPv6 sanity - 'code' must be 0 (RFC 4861)" + if not ip6_hop == 255: + return "ICMPv6 sanity - 'hop' must be 255 (RFC 4861)" + if not (ip6_src.is_unicast or ip6_src.is_unspecified): + return "ICMPv6 sanity - 'src' must be unicast or unspecified (RFC 4861)" + if not ip6_dst == IPv6Address("ff02::2"): + return "ICMPv6 sanity - 'dst' must be all-routers (RFC 4861)" + if ip6_src.is_unspecified and self.nd_opt_slla: + return "ICMPv6 sanity - 'nd_opt_slla' must not be included if 'src' is unspecified (RFC 4861)" + + elif self.type == icmp6.ps.ROUTER_ADVERTISEMENT: + if not self.code == 0: + return "ICMPv6 sanity - 'code' must be 0 (RFC 4861)" + if not ip6_hop == 255: + return "ICMPv6 sanity - 'hop' must be 255 (RFC 4861)" + if not ip6_src.is_link_local: + return "ICMPv6 sanity - 'src' must be link local (RFC 4861)" + if not (ip6_dst.is_unicast or ip6_dst == IPv6Address("ff02::1")): + return "ICMPv6 sanity - 'dst' must be unicast or all-nodes (RFC 4861)" + + elif self.type == icmp6.ps.NEIGHBOR_SOLICITATION: + if not self.code == 0: + return "ICMPv6 sanity - 'code' must be 0 (RFC 4861)" + if not ip6_hop == 255: + return "ICMPv6 sanity - 'hop' must be 255 (RFC 4861)" + if not (ip6_src.is_unicast or ip6_src.is_unspecified): + return "ICMPv6 sanity - 'src' must be unicast or unspecified (RFC 4861)" + if ip6_dst not in {self.ns_target_address, self.ns_target_address.solicited_node_multicast}: + return "ICMPv6 sanity - 'dst' must be 'ns_target_address' or it's solicited-node multicast (RFC 4861)" + if not self.ns_target_address.is_unicast: + return "ICMPv6 sanity - 'ns_target_address' must be unicast (RFC 4861)" + if ip6_src.is_unspecified and self.nd_opt_slla is not None: + return "ICMPv6 sanity - 'nd_opt_slla' must not be included if 'src' is unspecified" + + elif self.type == icmp6.ps.NEIGHBOR_ADVERTISEMENT: + if not self.code == 0: + return "ICMPv6 sanity - 'code' must be 0 (RFC 4861)" + if not ip6_hop == 255: + return "ICMPv6 sanity - 'hop' must be 255 (RFC 4861)" + if not ip6_src.is_unicast: + return "ICMPv6 sanity - 'src' must be unicast (RFC 4861)" + if self.na_flag_s is True and not (ip6_dst.is_unicast or ip6_dst == IPv6Address("ff02::1")): + return "ICMPv6 sanity - if 'na_flag_s' is set then 'dst' must be unicast or all-nodes (RFC 4861)" + if self.na_flag_s is False and not ip6_dst == IPv6Address("ff02::1"): + return "ICMPv6 sanity - if 'na_flag_s' is not set then 'dst' must be all-nodes (RFC 4861)" + + elif self.type == icmp6.ps.MLD2_REPORT: + if not self.code == 0: + return "ICMPv6 sanity - 'code' must be 0 (RFC 3810)" + if not ip6_hop == 1: + return "ICMPv6 sanity - 'hop' must be 1 (RFC 3810)" + + return "" + + +# +# ICMPv6 Neighbor Discovery options +# + + +class NdOptSLLA(icmp6.ps.NdOptSLLA): + """ICMPv6 ND option - Source Link Layer Address (1)""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.code = frame[optr + 0] + self.len = frame[optr + 1] << 3 + self.slla = ":".join([f"{_:0>2x}" for _ in frame[optr + 2 : optr + 8]]) + + +class NdOptTLLA(icmp6.ps.NdOptTLLA): + """ICMPv6 ND option - Target Link Layer Address (2)""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.code = frame[optr + 0] + self.len = frame[optr + 1] << 3 + self.tlla = ":".join([f"{_:0>2x}" for _ in frame[optr + 2 : optr + 8]]) + + +class NdOptPI(icmp6.ps.NdOptPI): + """ICMPv6 ND option - Prefix Information (3)""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.code = frame[optr + 0] + self.len = frame[optr + 1] << 3 + self.flag_l = bool(frame[optr + 3] & 0b10000000) + self.flag_a = bool(frame[optr + 3] & 0b01000000) + self.flag_r = bool(frame[optr + 3] & 0b00100000) + self.valid_lifetime = struct.unpack_from("!L", frame, optr + 4)[0] + self.preferred_lifetime = struct.unpack_from("!L", frame, optr + 8)[0] + self.prefix = IPv6Network((frame[optr + 16 : optr + 32], frame[optr + 2])) + + +class NdOptUnk(icmp6.ps.NdOptUnk): + """ICMPv6 ND option not supported by this stack""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.code = frame[optr + 0] + self.len = frame[optr + 1] << 3 + self.data = frame[optr + 2 : optr + self.len] + + def __str__(self): + return f"unk-{self.code}-{self.len}" + + +# +# ICMPv6 Multicast support classes +# + + +class MulticastAddressRecord: + """Multicast Address Record used by MLDv2 Report message""" + + def __init__(self, raw_record: bytes) -> None: + """Class constructor""" + + self.raw_record = raw_record + self.record_type = self.raw_record[0] + self.aux_data_len = self.raw_record[1] + self.number_of_sources = struct.unpack("!H", self.raw_record[2:4])[0] + self.multicast_address = IPv6Address(self.raw_record[4:20]) + self.source_address = [IPv6Address(self.raw_record[20 + 16 * _ : 20 + 16 * (_ + 1)]) for _ in range(self.number_of_sources)] + self.aux_data = self.raw_record[20 + 16 * self.number_of_sources :] + + def __len__(self) -> int: + """Length of raw record""" + + return len(self.raw_record) + + def __hash__(self) -> int: + """Hash of raw record""" + + return hash(self.raw_record) + + def __eq__(self, other) -> bool: + """Compare two records""" + + return self.raw_record == other.raw_record diff --git a/phrx_icmp6.py b/icmp6/phrx.py similarity index 75% rename from phrx_icmp6.py rename to icmp6/phrx.py index db884762..f4b28efc 100755 --- a/phrx_icmp6.py +++ b/icmp6/phrx.py @@ -23,33 +23,27 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phrx_icmp6.py - packet handler for inbound ICMPv6 packets +# icmp6/phrx.py - packet handler for inbound ICMPv6 packets # -import fpa_icmp6 -import fpp_icmp6 -from ipv6_address import IPv6Address +from typing import cast + +import icmp6.fpa +import icmp6.fpp +import icmp6.ps +from icmp6.fpp import Parser as Icmp6Parser +from ip6.fpp import Parser as Ip6Parser +from misc.ipv6_address import IPv6Address +from misc.packet import PacketRx -def _phrx_icmp6(self, packet_rx): +def _phrx_icmp6(self, packet_rx: PacketRx) -> None: """Handle inbound ICMPv6 packets""" - fpp_icmp6.Icmp6Packet(packet_rx) + icmp6.fpp.Parser(packet_rx) if packet_rx.parse_failed: if __debug__: @@ -59,9 +53,11 @@ def _phrx_icmp6(self, packet_rx): if __debug__: self._logger.opt(ansi=True).info(f"{packet_rx.tracker} - {packet_rx.icmp6}") - # ICMPv6 Neighbor Solicitation packet - if packet_rx.icmp6.type == fpp_icmp6.ICMP6_NEIGHBOR_SOLICITATION: + packet_rx.ip6 = cast(Ip6Parser, packet_rx.ip6) + # ICMPv6 Neighbor Solicitation packet + packet_rx.icmp6 = cast(Icmp6Parser, packet_rx.icmp6) + if packet_rx.icmp6.type == icmp6.ps.NEIGHBOR_SOLICITATION: # Check if request is for one of stack's IPv6 unicast addresses if packet_rx.icmp6.ns_target_address not in self.ip6_unicast: if __debug__: @@ -85,19 +81,19 @@ def _phrx_icmp6(self, packet_rx): ip6_src=packet_rx.icmp6.ns_target_address, ip6_dst=IPv6Address("ff02::1") if ip6_nd_dad else packet_rx.ip6.src, # use ff02::1 destination addriess when responding to DAD equest ip6_hop=255, - icmp6_type=fpp_icmp6.ICMP6_NEIGHBOR_ADVERTISEMENT, + icmp6_type=icmp6.ps.NEIGHBOR_ADVERTISEMENT, icmp6_na_flag_s=not ip6_nd_dad, # no S flag when responding to DAD request icmp6_na_flag_o=ip6_nd_dad, # O flag when respondidng to DAD request (this is not necessary but Linux uses it) icmp6_na_target_address=packet_rx.icmp6.ns_target_address, - icmp6_nd_options=[fpa_icmp6.Icmp6NdOptTLLA(self.mac_unicast)], + icmp6_nd_options=[icmp6.fpa.NdOptTLLA(self.mac_unicast)], echo_tracker=packet_rx.tracker, ) return # ICMPv6 Neighbor Advertisement packet - if packet_rx.icmp6.type == fpp_icmp6.ICMP6_NEIGHBOR_ADVERTISEMENT: - + if packet_rx.icmp6.type == icmp6.ps.NEIGHBOR_ADVERTISEMENT: if __debug__: + packet_rx.icmp6 = cast(Icmp6Parser, packet_rx.icmp6) self._logger.debug(f"Received ICMPv6 Neighbor Advertisement packet for {packet_rx.icmp6.na_target_address} from {packet_rx.ip6.src}") # Run ND Duplicate Address Detection check @@ -114,14 +110,14 @@ def _phrx_icmp6(self, packet_rx): return # ICMPv6 Router Solicitaion packet (this is not currently used by the stack) - if packet_rx.icmp6.type == fpp_icmp6.ICMP6_ROUTER_SOLICITATION: + if packet_rx.icmp6.type == icmp6.ps.ROUTER_SOLICITATION: if __debug__: self._logger.debug(f"Received ICMPv6 Router Advertisement packet from {packet_rx.ip6.src}") return # ICMPv6 Router Advertisement packet - if packet_rx.icmp6.type == fpp_icmp6.ICMP6_ROUTER_ADVERTISEMENT: + if packet_rx.icmp6.type == icmp6.ps.ROUTER_ADVERTISEMENT: if __debug__: self._logger.debug(f"Received ICMPv6 Router Advertisement packet from {packet_rx.ip6.src}") @@ -132,7 +128,7 @@ def _phrx_icmp6(self, packet_rx): return # Respond to ICMPv6 Echo Request packet - if packet_rx.icmp6.type == fpp_icmp6.ICMP6_ECHO_REQUEST: + if packet_rx.icmp6.type == icmp6.ps.ECHO_REQUEST: if __debug__: self._logger.debug(f"Received ICMPv6 Echo Request packet from {packet_rx.ip6.src}, sending reply") @@ -140,7 +136,7 @@ def _phrx_icmp6(self, packet_rx): ip6_src=packet_rx.ip6.dst, ip6_dst=packet_rx.ip6.src, ip6_hop=255, - icmp6_type=fpp_icmp6.ICMP6_ECHO_REPLY, + icmp6_type=icmp6.ps.ECHO_REPLY, icmp6_ec_id=packet_rx.icmp6.ec_id, icmp6_ec_seq=packet_rx.icmp6.ec_seq, icmp6_ec_data=packet_rx.icmp6.ec_data, diff --git a/phtx_icmp6.py b/icmp6/phtx.py similarity index 64% rename from phtx_icmp6.py rename to icmp6/phtx.py index 493f584e..227e8da8 100755 --- a/phtx_icmp6.py +++ b/icmp6/phtx.py @@ -23,55 +23,47 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phtx_icmp6.py - packet handler for outbound ICMPv6 packets +# icmp6/phtx.py - packet handler for outbound ICMPv6 packets # +from typing import Optional, Union + import config -import fpa_icmp6 +import icmp6.fpa +from misc.ipv6_address import IPv6Address +from misc.tracker import Tracker def _phtx_icmp6( self, - ip6_src, - ip6_dst, - icmp6_type, - icmp6_code=0, - ip6_hop=64, - icmp6_un_data=None, - icmp6_ec_id=None, - icmp6_ec_seq=None, - icmp6_ec_data=None, - icmp6_ns_target_address=None, - icmp6_na_flag_r=False, - icmp6_na_flag_s=False, - icmp6_na_flag_o=False, - icmp6_na_target_address=None, - icmp6_nd_options=None, + ip6_src: IPv6Address, + ip6_dst: IPv6Address, + icmp6_type: int, + icmp6_code: int = 0, + ip6_hop: int = 64, + icmp6_un_data: Optional[bytes] = None, + icmp6_ec_id: Optional[int] = None, + icmp6_ec_seq: Optional[int] = None, + icmp6_ec_data: Optional[bytes] = None, + icmp6_ns_target_address: Optional[IPv6Address] = None, + icmp6_na_flag_r: bool = False, + icmp6_na_flag_s: bool = False, + icmp6_na_flag_o: bool = False, + icmp6_na_target_address: Optional[IPv6Address] = None, + icmp6_nd_options: Optional[list[Union[icmp6.fpa.NdOptSLLA, icmp6.fpa.NdOptTLLA, icmp6.fpa.NdOptPI]]] = None, icmp6_mlr2_multicast_address_record=None, - echo_tracker=None, -): + echo_tracker: Optional[Tracker] = None, +) -> None: """Handle outbound ICMPv6 packets""" # Check if IPv6 protocol support is enabled, if not then silently drop the packet if not config.ip6_support: return - icmp6_packet_tx = fpa_icmp6.Icmp6Packet( + icmp6_packet_tx = icmp6.fpa.Assembler( type=icmp6_type, code=icmp6_code, un_data=icmp6_un_data, @@ -90,4 +82,4 @@ def _phtx_icmp6( if __debug__: self._logger.opt(ansi=True).info(f"{icmp6_packet_tx.tracker} - {icmp6_packet_tx}") - self._phtx_ip6(ip6_src=ip6_src, ip6_dst=ip6_dst, ip6_hop=ip6_hop, child_packet=icmp6_packet_tx) + self._phtx_ip6(ip6_src=ip6_src, ip6_dst=ip6_dst, ip6_hop=ip6_hop, carried_packet=icmp6_packet_tx) diff --git a/fpa_icmp6.py b/icmp6/ps.py similarity index 57% rename from fpa_icmp6.py rename to icmp6/ps.py index a94fbb4d..16d2fee5 100755 --- a/fpa_icmp6.py +++ b/icmp6/ps.py @@ -23,29 +23,12 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# fpa_icmp6.py - Fast Packet Assembler support class for ICMPv6 protocol +# icmp6/ps.py - protocol support for ICMPv6 # - -import struct - -from ip_helper import inet_cksum -from ipv6_address import IPv6Address, IPv6Network -from tracker import Tracker +from misc.ipv6_address import IPv6Network # Destination Unreachable message (1/[0-6]) @@ -292,276 +275,93 @@ # ~ ~ # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -ICMP6_UNREACHABLE = 1 -ICMP6_UNREACHABLE_LEN = 8 -ICMP6_UNREACHABLE__NO_ROUTE = 0 -ICMP6_UNREACHABLE__PROHIBITED = 1 -ICMP6_UNREACHABLE__SCOPE = 2 -ICMP6_UNREACHABLE__ADDRESS = 3 -ICMP6_UNREACHABLE__PORT = 4 -ICMP6_UNREACHABLE__FAILED_POLICY = 5 -ICMP6_UNREACHABLE__REJECT_ROUTE = 6 -ICMP6_PACKET_TOO_BIG = 2 -ICMP6_PACKET_TOO_BIG_LEN = 8 -ICMP6_TIME_EXCEEDED = 3 -ICMP6_TIME_EXCEEDED_LEN = 8 -ICMP6_PARAMETER_PROBLEM = 4 -ICMP6_PARAMETER_PROBLEM_LEN = 8 -ICMP6_ECHO_REQUEST = 128 -ICMP6_ECHO_REQUEST_LEN = 8 -ICMP6_ECHO_REPLY = 129 -ICMP6_ECHO_REPLY_LEN = 8 -ICMP6_MLD2_QUERY = 130 -ICMP6_MLD2_QUERY_LEN = 28 -ICMP6_ROUTER_SOLICITATION = 133 -ICMP6_ROUTER_SOLICITATION_LEN = 8 -ICMP6_ROUTER_ADVERTISEMENT = 134 -ICMP6_ROUTER_ADVERTISEMENT_LEN = 16 -ICMP6_NEIGHBOR_SOLICITATION = 135 -ICMP6_NEIGHBOR_SOLICITATION_LEN = 24 -ICMP6_NEIGHBOR_ADVERTISEMENT = 136 -ICMP6_NEIGHBOR_ADVERTISEMENT_LEN = 24 -ICMP6_MLD2_REPORT = 143 -ICMP6_MLD2_REPORT_LEN = 8 - - -ICMP6_MART_MODE_IS_INCLUDE = 1 -ICMP6_MART_MODE_IS_EXCLUDE = 2 -ICMP6_MART_CHANGE_TO_INCLUDE = 3 -ICMP6_MART_CHANGE_TO_EXCLUDE = 4 -ICMP6_MART_ALLOW_NEW_SOURCES = 5 -ICMP6_MART_BLOCK_OLD_SOURCES = 6 - - -class Icmp6Packet: - """ICMPv6 packet support class""" - - protocol = "ICMP6" - - def __init__( - self, - type, - code=0, - un_data=b"", - ec_id=None, - ec_seq=None, - ec_data=b"", - ra_hop=None, - ra_flag_m=False, - ra_flag_o=False, - ra_router_lifetime=None, - ra_reachable_time=None, - ra_retrans_timer=None, - ns_target_address=None, - na_flag_r=False, - na_flag_s=False, - na_flag_o=False, - na_target_address=None, - nd_options=None, - mlr2_multicast_address_record=None, - echo_tracker=None, - ): - """Class constructor""" - - self.tracker = Tracker("TX", echo_tracker) - - self.type = type - self.code = code - - self.nd_options = [] if nd_options is None else nd_options - - if self.type == ICMP6_UNREACHABLE: - self.un_reserved = 0 - self.un_data = un_data[:520] - - elif self.type == ICMP6_ECHO_REQUEST: - self.ec_id = ec_id - self.ec_seq = ec_seq - self.ec_data = ec_data - - elif self.type == ICMP6_ECHO_REPLY: - self.ec_id = ec_id - self.ec_seq = ec_seq - self.ec_data = ec_data - - elif self.type == ICMP6_ROUTER_SOLICITATION: - self.rs_reserved = 0 - - elif self.type == ICMP6_ROUTER_ADVERTISEMENT: - self.ra_hop = ra_hop - self.ra_flag_m = ra_flag_m - self.ra_flag_o = ra_flag_o - self.ra_router_lifetime = ra_router_lifetime - self.ra_reachable_time = ra_reachable_time - self.ra_retrans_timer = ra_retrans_timer - - elif self.type == ICMP6_NEIGHBOR_SOLICITATION: - self.ns_reserved = 0 - self.ns_target_address = ns_target_address - - elif self.type == ICMP6_NEIGHBOR_ADVERTISEMENT: - self.na_flag_r = na_flag_r - self.na_flag_s = na_flag_s - self.na_flag_o = na_flag_o - self.na_reserved = 0 - self.na_target_address = na_target_address - - elif self.type == ICMP6_MLD2_REPORT: - self.mlr2_reserved = 0 - self.mlr2_multicast_address_record = [] if mlr2_multicast_address_record is None else mlr2_multicast_address_record - self.mlr2_number_of_multicast_address_records = len(self.mlr2_multicast_address_record) - - def __str__(self): - """Packet log string""" - - log = f"ICMPv6 type {self.type}, code {self.code}" - - if self.type == ICMP6_UNREACHABLE: - pass - - elif self.type == ICMP6_ECHO_REQUEST: - log += f", id {self.ec_id}, seq {self.ec_seq}" - - elif self.type == ICMP6_ECHO_REPLY: - log += f", id {self.ec_id}, seq {self.ec_seq}" - - elif self.type == ICMP6_ROUTER_SOLICITATION: - for nd_option in self.nd_options: - log += ", " + str(nd_option) - - elif self.type == ICMP6_ROUTER_ADVERTISEMENT: - log += f", hop {self.ra_hop}" - log += f"flags {'M' if self.ra_flag_m else '-'}{'O' if self.ra_flag_o else '-'}" - log += f"rlft {self.ra_router_lifetime}, reacht {self.ra_reachable_time}, retrt {self.ra_retrans_timer}" - for nd_option in self.nd_options: - log += ", " + str(nd_option) - - elif self.type == ICMP6_NEIGHBOR_SOLICITATION: - log += f", target {self.ns_target_address}" - for nd_option in self.nd_options: - log += ", " + str(nd_option) - - elif self.type == ICMP6_NEIGHBOR_ADVERTISEMENT: - log += f", target {self.na_target_address}" - log += f", flags {'R' if self.na_flag_r else '-'}{'S' if self.na_flag_s else '-'}{'O' if self.na_flag_o else '-'}" - for nd_option in self.nd_options: - log += ", " + str(nd_option) - - elif self.type == ICMP6_MLD2_REPORT: - pass - - return log - - def __len__(self): - """Length of the packet""" - - if self.type == ICMP6_UNREACHABLE: - return ICMP6_UNREACHABLE_LEN + len(self.un_data) - - if self.type == ICMP6_ECHO_REQUEST: - return ICMP6_ECHO_REQUEST_LEN + len(self.ec_data) - - if self.type == ICMP6_ECHO_REPLY: - return ICMP6_ECHO_REPLY_LEN + len(self.ec_data) - - if self.type == ICMP6_ROUTER_SOLICITATION: - return ICMP6_ROUTER_SOLICITATION_LEN + sum([len(_) for _ in self.nd_options]) - - if self.type == ICMP6_ROUTER_ADVERTISEMENT: - return ICMP6_ROUTER_ADVERTISEMENT_LEN + sum([len(_) for _ in self.nd_options]) - - if self.type == ICMP6_NEIGHBOR_SOLICITATION: - return ICMP6_NEIGHBOR_SOLICITATION_LEN + sum([len(_) for _ in self.nd_options]) - - if self.type == ICMP6_NEIGHBOR_ADVERTISEMENT: - return ICMP6_NEIGHBOR_ADVERTISEMENT_LEN + sum([len(_) for _ in self.nd_options]) - - if self.type == ICMP6_MLD2_REPORT: - return ICMP6_MLD2_REPORT_LEN + sum([len(_) for _ in self.mlr2_multicast_address_record]) - - def assemble_packet(self, frame, hptr, pshdr_sum): - """Assemble packet into the raw form""" - - if self.type == ICMP6_UNREACHABLE: - struct.pack_into(f"! BBH L {len(self.un_data)}s", frame, hptr, self.type, self.code, 0, self.un_reserved, self.un_data) - - elif self.type == ICMP6_ECHO_REQUEST: - struct.pack_into(f"! BBH HH {len(self.ec_data)}s", frame, hptr, self.type, self.code, 0, self.ec_id, self.ec_seq, self.ec_data) - - elif self.type == ICMP6_ECHO_REPLY: - struct.pack_into(f"! BBH HH {len(self.ec_data)}s", frame, hptr, self.type, self.code, 0, self.ec_id, self.ec_seq, self.ec_data) - - elif self.type == ICMP6_ROUTER_SOLICITATION: - struct.pack_into(f"! BBH L {len(self.raw_nd_options)}s", frame, hptr, self.type, self.code, 0, self.rs_reserved, self.raw_nd_options) - - elif self.type == ICMP6_ROUTER_ADVERTISEMENT: - struct.pack_into( - f"! BBH BBH L L {len(self.raw_nd_options)}s", - frame, - hptr, - self.type, - self.code, - 0, - self.ra_hop, - (self.ra_flag_m << 7) | (self.ra_flag_o << 6), - self.ra_router_lifetime, - self.ra_reachable_time, - self.ra_retrans_timer, - self.raw_nd_options, - ) - - elif self.type == ICMP6_NEIGHBOR_SOLICITATION: - struct.pack_into( - f"! BBH L 16s {len(self.raw_nd_options)}s", - frame, - hptr, - self.type, - self.code, - 0, - self.ns_reserved, - self.ns_target_address.packed, - self.raw_nd_options, - ) - - elif self.type == ICMP6_NEIGHBOR_ADVERTISEMENT: - struct.pack_into( - f"! BBH L 16s {len(self.raw_nd_options)}s", - frame, - hptr, - self.type, - self.code, - 0, - (self.na_flag_r << 31) | (self.na_flag_s << 30) | (self.na_flag_o << 29) | self.na_reserved, - self.na_target_address.packed, - self.raw_nd_options, - ) - - elif self.type == ICMP6_MLD2_REPORT: - struct.pack_into( - f"! BBH HH {sum([len(_) for _ in self.mlr2_multicast_address_record])}s", - frame, - hptr, - self.type, - self.code, - 0, - self.mlr2_reserved, - self.mlr2_number_of_multicast_address_records, - b"".join([_.raw_record for _ in self.mlr2_multicast_address_record]), - ) - - struct.pack_into("! H", frame, hptr + 2, inet_cksum(frame, hptr, len(self), pshdr_sum)) - - @property - def raw_nd_options(self): - """ICMPv6 ND packet options in raw format""" - - raw_nd_options = b"" - - for option in self.nd_options: - raw_nd_options += option.raw_option - - return raw_nd_options +HEADER_LEN = 4 + +UNREACHABLE = 1 +UNREACHABLE_LEN = 8 +UNREACHABLE__NO_ROUTE = 0 +UNREACHABLE__PROHIBITED = 1 +UNREACHABLE__SCOPE = 2 +UNREACHABLE__ADDRESS = 3 +UNREACHABLE__PORT = 4 +UNREACHABLE__FAILED_POLICY = 5 +UNREACHABLE__REJECT_ROUTE = 6 +PACKET_TOO_BIG = 2 +PACKET_TOO_BIG_LEN = 8 +TIME_EXCEEDED = 3 +TIME_EXCEEDED_LEN = 8 +PARAMETER_PROBLEM = 4 +PARAMETER_PROBLEM_LEN = 8 +ECHO_REQUEST = 128 +ECHO_REQUEST_LEN = 8 +ECHO_REPLY = 129 +ECHO_REPLY_LEN = 8 +MLD2_QUERY = 130 +MLD2_QUERY_LEN = 28 +ROUTER_SOLICITATION = 133 +ROUTER_SOLICITATION_LEN = 8 +ROUTER_ADVERTISEMENT = 134 +ROUTER_ADVERTISEMENT_LEN = 16 +NEIGHBOR_SOLICITATION = 135 +NEIGHBOR_SOLICITATION_LEN = 24 +NEIGHBOR_ADVERTISEMENT = 136 +NEIGHBOR_ADVERTISEMENT_LEN = 24 +MLD2_REPORT = 143 +MLD2_REPORT_LEN = 8 + + +MART_MODE_IS_INCLUDE = 1 +MART_MODE_IS_EXCLUDE = 2 +MART_CHANGE_TO_INCLUDE = 3 +MART_CHANGE_TO_EXCLUDE = 4 +MART_ALLOW_NEW_SOURCES = 5 +MART_BLOCK_OLD_SOURCES = 6 + + +def __str__(self) -> str: + """Packet log string""" + + log = f"ICMPv6 type {self.type}, code {self.code}" + + if self.type == UNREACHABLE: + pass + + elif self.type == ECHO_REQUEST: + log += f", id {self.ec_id}, seq {self.ec_seq}" + + elif self.type == ECHO_REPLY: + log += f", id {self.ec_id}, seq {self.ec_seq}" + + elif self.type == ROUTER_SOLICITATION: + assert self.nd_options is not None + for nd_option in self.nd_options: + log += ", " + str(nd_option) + + elif self.type == ROUTER_ADVERTISEMENT: + assert self.nd_options is not None + log += f", hop {self.ra_hop}" + log += f"flags {'M' if self.ra_flag_m else '-'}{'O' if self.ra_flag_o else '-'}" + log += f"rlft {self.ra_router_lifetime}, reacht {self.ra_reachable_time}, retrt {self.ra_retrans_timer}" + for nd_option in self.nd_options: + log += ", " + str(nd_option) + + elif self.type == NEIGHBOR_SOLICITATION: + assert self.nd_options is not None + log += f", target {self.ns_target_address}" + for nd_option in self.nd_options: + log += ", " + str(nd_option) + + elif self.type == NEIGHBOR_ADVERTISEMENT: + assert self.nd_options is not None + log += f", target {self.na_target_address}" + log += f", flags {'R' if self.na_flag_r else '-'}{'S' if self.na_flag_s else '-'}{'O' if self.na_flag_o else '-'}" + for nd_option in self.nd_options: + log += ", " + str(nd_option) + + elif self.type == MLD2_REPORT: + pass + + return log # @@ -577,25 +377,27 @@ def raw_nd_options(self): # > MAC Address | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -ICMP6_ND_OPT_SLLA = 1 -ICMP6_ND_OPT_SLLA_LEN = 8 +ND_OPT_SLLA = 1 +ND_OPT_SLLA_LEN = 8 -class Icmp6NdOptSLLA: +class NdOptSLLA: """ICMPv6 ND option - Source Link Layer Address (1)""" - def __init__(self, slla): - self.slla = slla + def __init__(self) -> None: + """Class constructor""" - @property - def raw_option(self): - return struct.pack("! BB 6s", ICMP6_ND_OPT_SLLA, ICMP6_ND_OPT_SLLA_LEN >> 3, bytes.fromhex(self.slla.replace(":", ""))) + self.slla = "not initialised" + + def __str__(self) -> str: + """Option log string""" - def __str__(self): return f"slla {self.slla}" - def __len__(self): - return ICMP6_ND_OPT_SLLA_LEN + def __len__(self) -> int: + """Option length""" + + return ND_OPT_SLLA_LEN # ICMPv6 ND option - Target Link Layer Address (2) @@ -606,25 +408,27 @@ def __len__(self): # > MAC Address | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -ICMP6_ND_OPT_TLLA = 2 -ICMP6_ND_OPT_TLLA_LEN = 8 +ND_OPT_TLLA = 2 +ND_OPT_TLLA_LEN = 8 -class Icmp6NdOptTLLA: +class NdOptTLLA: """ICMPv6 ND option - Target Link Layer Address (2)""" - def __init__(self, tlla): - self.tlla = tlla + def __init__(self) -> None: + """Class constructor""" + + self.tlla = "not initialised" - @property - def raw_option(self): - return struct.pack("! BB 6s", ICMP6_ND_OPT_TLLA, ICMP6_ND_OPT_TLLA_LEN >> 3, bytes.fromhex(self.tlla.replace(":", ""))) + def __str__(self) -> str: + """Option log string""" - def __str__(self): return f"tlla {self.tlla}" - def __len__(self): - return ICMP6_ND_OPT_TLLA_LEN + def __len__(self) -> int: + """Option length""" + + return ND_OPT_TLLA_LEN # ICMPv6 ND option - Prefix Information (3) @@ -647,90 +451,42 @@ def __len__(self): # | | # +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -ICMP6_ND_OPT_PI = 3 -ICMP6_ND_OPT_PI_LEN = 32 +ND_OPT_PI = 3 +ND_OPT_PI_LEN = 32 -class Icmp6NdOptPI: +class NdOptPI: """ICMPv6 ND option - Prefix Information (3)""" - def __init__( - self, - flag_l=False, - flag_a=False, - flag_r=False, - valid_lifetime=None, - preferred_lifetime=None, - prefix=None, - ): - self.code = ICMP6_ND_OPT_PI - self.len = ICMP6_ND_OPT_PI_LEN - self.flag_l = flag_l - self.flag_a = flag_a - self.flag_r = flag_r - self.valid_lifetime = valid_lifetime - self.preferred_lifetime = preferred_lifetime - self.prefix = IPv6Network(prefix) - - @property - def raw_option(self): - return struct.pack( - "! BB BB L L L 16s", - self.code, - self.len >> 3, - self.prefix.prefixlen, - (self.flag_l << 7) | (self.flag_a << 6) | (self.flag_r << 6), - self.valid_lifetime, - self.preferred_lifetime, - self.prefix.network_address.packed, - ) - - def __str__(self): - return f"prefix_info {self.prefix}" - - def __len__(self): - return ICMP6_ND_OPT_PI_LEN - + def __init__(self) -> None: + """Class constructor""" -# -# ICMPv6 Multicast support classes -# + self.prefix = IPv6Network(0) + def __str__(self) -> str: + """Option log string""" -class MulticastAddressRecord: - """Multicast Address Record used by MLDv2 Report message""" + return f"prefix_info {self.prefix}" - def __init__(self, record_type, multicast_address, source_address=None, aux_data=b""): - """Class constructor""" + def __len__(self) -> int: + """Option length""" - self.record_type = record_type - self.aux_data_len = len(aux_data) - self.multicast_address = IPv6Address(multicast_address) - self.source_address = [] if source_address is None else source_address - self.number_of_sources = len(self.source_address) - self.aux_data = aux_data + return ND_OPT_PI_LEN - def __len__(self): - """Length of raw record""" - return len(self.raw_record) +# ICMPv6 ND unknown option - def __hash__(self): - """Hash of raw record""" - return hash(self.raw_record) +class NdOptUnk: + """ICMPv6 ND option not supported by this stack""" - def __eq__(self, other): - """Compare two records""" + def __init__(self) -> None: + """Class constructor""" - return self.raw_record == other.raw_record + self.code = -1 + self.len = -1 - @property - def raw_record(self): - """Get record in raw format""" + def __str__(self) -> str: + """Option log string""" - return ( - struct.pack("! BBH 16s", self.record_type, self.aux_data_len, self.number_of_sources, self.multicast_address.packed) - + b"".join([_.packed for _ in self.source_address]) - + self.aux_data - ) + return f"unk-{self.code}-{self.len}" diff --git a/ip4/__init__.py b/ip4/__init__.py new file mode 100755 index 00000000..7da48334 --- /dev/null +++ b/ip4/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ip4/__init__.py +# diff --git a/fpa_ip4.py b/ip4/fpa.py similarity index 51% rename from fpa_ip4.py rename to ip4/fpa.py index e0c3f7aa..73fbee55 100755 --- a/fpa_ip4.py +++ b/ip4/fpa.py @@ -23,81 +23,49 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# fpa_ip4.py - Fast Packet Assembler support class for IPv4 protocol +# ip4/fpa.py - Fast Packet Assembler support class for IPv4 protocol # + import struct +from typing import Optional, Union import config -from ip_helper import inet_cksum -from ipv4_address import IPv4Address -from tracker import Tracker - -# IPv4 protocol header - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# |Version| IHL | DSCP |ECN| Total Length | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Identification |Flags| Fragment Offset | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Time to Live | Protocol | Header Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Source Address | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Destination Address | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# ~ Options ~ Padding ~ -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -IP4_HEADER_LEN = 20 +import ether.ps +import icmp4.fpa +import ip4.ps +import tcp.fpa +import udp.fpa +from misc.ip_helper import inet_cksum +from misc.ipv4_address import IPv4Address +from misc.tracker import Tracker -IP4_PROTO_ICMP4 = 1 -IP4_PROTO_TCP = 6 -IP4_PROTO_UDP = 17 +class Assembler: + """IPv4 packet assembler support class""" -IP4_PROTO_TABLE = {IP4_PROTO_ICMP4: "ICMPv4", IP4_PROTO_TCP: "TCP", IP4_PROTO_UDP: "UDP"} - - -class Ip4Packet: - """IPv4 packet support class""" - - protocol = "IP4" + ether_type = ether.ps.TYPE_IP4 def __init__( self, - child_packet, - src, - dst, - ttl=config.ip4_default_ttl, - dscp=0, - ecn=0, - id=0, - flag_df=False, - options=None, - ): + carried_packet: Union[icmp4.fpa.Assembler, tcp.fpa.Assembler, udp.fpa.Assembler], + src: IPv4Address, + dst: IPv4Address, + ttl: int = config.ip4_default_ttl, + dscp: int = 0, + ecn: int = 0, + id: int = 0, + flag_df: bool = False, + options: Optional[list] = None, + ) -> None: """Class constructor""" - assert child_packet.protocol in {"ICMP4", "UDP", "TCP"}, f"Not supported protocol: {child_packet.protocol}" - self._child_packet = child_packet - - self.tracker = self._child_packet.tracker + assert carried_packet.ip4_proto in {ip4.ps.PROTO_ICMP4, ip4.ps.PROTO_UDP, ip4.ps.PROTO_TCP} + self._carried_packet = carried_packet + self.tracker = self._carried_packet.tracker self.ver = 4 self.dscp = dscp self.ecn = ecn @@ -108,37 +76,27 @@ def __init__( self.ttl = ttl self.src = IPv4Address(src) self.dst = IPv4Address(dst) - self.options = [] if options is None else options - - self.hlen = IP4_HEADER_LEN + len(self.raw_options) + self.hlen = ip4.ps.HEADER_LEN + len(self.raw_options) self.plen = len(self) + self.proto = self._carried_packet.ip4_proto - if self._child_packet.protocol == "ICMP4": - self.proto = IP4_PROTO_ICMP4 - - if self._child_packet.protocol == "UDP": - self.proto = IP4_PROTO_UDP + def __len__(self): + """Length of the packet""" - if self._child_packet.protocol == "TCP": - self.proto = IP4_PROTO_TCP + return ip4.ps.HEADER_LEN + sum([len(_) for _ in self.options]) + len(self._carried_packet) - def __str__(self): + def __str__(self) -> str: """Packet log string""" return ( - f"IPv4 {self.src} > {self.dst}, proto {self.proto} ({IP4_PROTO_TABLE.get(self.proto, '???')}), id {self.id}" + f"IPv4 {self.src} > {self.dst}, proto {self.proto} ({ip4.ps.PROTO_TABLE.get(self.proto, '???')}), id {self.id}" + f"{', DF' if self.flag_df else ''}{', MF' if self.flag_mf else ''}, offset {self.offset}, plen {self.plen}" + f", ttl {self.ttl}" ) - def __len__(self): - """Length of the packet""" - - return IP4_HEADER_LEN + sum([len(_) for _ in self.options]) + len(self._child_packet) - @property - def raw_options(self): + def raw_options(self) -> bytes: """Packet options in raw format""" raw_options = b"" @@ -149,19 +107,19 @@ def raw_options(self): return raw_options @property - def dlen(self): + def dlen(self) -> int: """Calculate data length""" return self.plen - self.hlen @property - def pshdr_sum(self): + def pshdr_sum(self) -> int: """Create IPv4 pseudo header used by TCP and UDP to compute their checksums""" pseudo_header = struct.pack("! 4s 4s BBH", self.src.packed, self.dst.packed, 0, self.proto, self.plen - self.hlen) return sum(struct.unpack("! 3L", pseudo_header)) - def assemble_packet(self, frame, hptr): + def assemble(self, frame, hptr) -> None: """Assemble packet into the raw form""" struct.pack_into( @@ -183,32 +141,33 @@ def assemble_packet(self, frame, hptr): struct.pack_into("! H", frame, hptr + 10, inet_cksum(frame, hptr, self.hlen)) - self._child_packet.assemble_packet(frame, hptr + self.hlen, self.pshdr_sum) + self._carried_packet.assemble(frame, hptr + self.hlen, self.pshdr_sum) -class Ip4Frag: - """IPv4 packet fragment support class""" +class FragAssembler: + """IPv4 packet fragment assembler support class""" - protocol = "IP4" + ether_type = ether.ps.TYPE_IP4 def __init__( self, - data, - proto, - src, - dst, - ttl=config.ip4_default_ttl, - dscp=0, - ecn=0, - id=0, - flag_mf=False, - offset=0, - options=None, + data: bytes, + proto: int, + src: IPv4Address, + dst: IPv4Address, + ttl: int = config.ip4_default_ttl, + dscp: int = 0, + ecn: int = 0, + id: int = 0, + flag_mf: bool = False, + offset: int = 0, + options: Optional[list] = None, ): """Class constructor""" - self.tracker = Tracker("TX") + assert proto in {ip4.ps.PROTO_ICMP4, ip4.ps.PROTO_UDP, ip4.ps.PROTO_TCP} + self.tracker = Tracker("TX") self.ver = 4 self.dscp = dscp self.ecn = ecn @@ -219,30 +178,21 @@ def __init__( self.ttl = ttl self.src = IPv4Address(src) self.dst = IPv4Address(dst) - self.options = [] if options is None else options self.data = data self.proto = proto - - self.hlen = IP4_HEADER_LEN + len(self.raw_options) + self.hlen = ip4.ps.HEADER_LEN + len(self.raw_options) self.plen = len(self) - def __str__(self): - """Packet log string""" - - return ( - f"IPv4 frag {self.src} > {self.dst}, proto {self.proto} ({IP4_PROTO_TABLE.get(self.proto, '???')}), id {self.id}" - + f"{', DF' if self.flag_df else ''}{', MF' if self.flag_mf else ''}, offset {self.offset}, plen {self.plen}" - + f", ttl {self.ttl}" - ) - def __len__(self): """Length of the packet""" - return IP4_HEADER_LEN + sum([len(_) for _ in self.options]) + len(self.data) + return ip4.ps.HEADER_LEN + sum([len(_) for _ in self.options]) + len(self.data) + + from ip4.ps import __str__ @property - def raw_options(self): + def raw_options(self) -> bytes: """Packet options in raw format""" raw_options = b"" @@ -252,14 +202,7 @@ def raw_options(self): return raw_options - @property - def pshdr_sum(self): - """Create IPv4 pseudo header used by TCP and UDP to compute their checksums""" - - pseudo_header = struct.pack("! 4s 4s BBH", self.src.packed, self.dst.packed, 0, self.proto, self.plen - self.hlen) - return sum(struct.unpack("! 3L", pseudo_header)) - - def assemble_packet(self, frame, hptr): + def assemble(self, frame: bytearray, hptr: int) -> None: """Assemble packet into the raw form""" struct.pack_into( @@ -290,39 +233,25 @@ def assemble_packet(self, frame, hptr): # IPv4 option - End of Option Linst -IP4_OPT_EOL = 0 -IP4_OPT_EOL_LEN = 1 - -class Ip4OptEol: +class OptEol(ip4.ps.OptEol): """IP option - End of Option List""" @property - def raw_option(self): - return struct.pack("!B", IP4_OPT_EOL) + def raw_option(self) -> bytes: + """Get option in raw form""" - def __str__(self): - return "eol" - - def __len__(self): - return IP4_OPT_EOL_LEN + return struct.pack("!B", ip4.ps.OPT_EOL) # IPv4 option - No Operation (1) -IP4_OPT_NOP = 1 -IP4_OPT_NOP_LEN = 1 - -class Ip4OptNop: +class OptNop(ip4.ps.OptNop): """IP option - No Operation""" @property - def raw_option(self): - return struct.pack("!B", IP4_OPT_NOP) + def raw_option(self) -> bytes: + """Get option in raw form""" - def __str__(self): - return "nop" - - def __len__(self): - return IP4_OPT_NOP_LEN + return struct.pack("!B", ip4.ps.OPT_NOP) diff --git a/ip4/fpp.py b/ip4/fpp.py new file mode 100755 index 00000000..16cf7813 --- /dev/null +++ b/ip4/fpp.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ip4/fpp.py - Fast Packet Parser support class for IPv4 protocol +# + + +import struct + +import config +import ip4.ps +from misc.ip_helper import inet_cksum +from misc.ipv4_address import IPv4Address +from misc.packet import PacketRx + + +class Parser: + """IPv4 packet parser class""" + + def __init__(self, packet_rx: PacketRx) -> None: + """Class constructor""" + + packet_rx.ip4 = self + packet_rx.ip = self + + self._frame = packet_rx.frame + self._hptr = packet_rx.hptr + + packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() + + if not packet_rx.parse_failed: + packet_rx.hptr = self._hptr + self.hlen + + def __len__(self) -> int: + """Number of bytes remaining in the frame""" + + return len(self._frame) - self._hptr + + from ip4.ps import __str__ + + @property + def ver(self) -> int: + """Read 'Version' field""" + + if "_cache__ver" not in self.__dict__: + self._cache__ver = self._frame[self._hptr + 0] >> 4 + return self._cache__ver + + @property + def hlen(self) -> int: + """Read 'Header length' field""" + + if "_cache__hlen" not in self.__dict__: + self._cache__hlen = (self._frame[self._hptr + 0] & 0b00001111) << 2 + return self._cache__hlen + + @property + def dscp(self) -> int: + """Read 'DSCP' field""" + + if "_cache__dscp" not in self.__dict__: + self._cache__dscp = (self._frame[self._hptr + 1] & 0b11111100) >> 2 + return self._cache__dscp + + @property + def ecn(self) -> int: + """Read 'ECN' field""" + + if "_cache__ecn" not in self.__dict__: + self._cache__ecn = self._frame[self._hptr + 1] & 0b00000011 + return self._cache__ecn + + @property + def plen(self) -> int: + """Read 'Packet length' field""" + + if "_cache__plen" not in self.__dict__: + self._cache__plen = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] + return self._cache__plen + + @property + def id(self) -> int: + """Read 'Identification' field""" + + if "_cache__id" not in self.__dict__: + self._cache__id = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] + return self._cache__id + + @property + def flag_df(self) -> bool: + """Read 'DF flag' field""" + + return bool(self._frame[self._hptr + 6] & 0b01000000) + + @property + def flag_mf(self) -> bool: + """Read 'MF flag' field""" + + return bool(self._frame[self._hptr + 6] & 0b00100000) + + @property + def offset(self) -> int: + """Read 'Fragment offset' field""" + + if "_cache__offset" not in self.__dict__: + self._cache__offset = (struct.unpack_from("!H", self._frame, self._hptr + 6)[0] & 0b0001111111111111) << 3 + return self._cache__offset + + @property + def ttl(self) -> int: + """Read 'TTL' field""" + + return self._frame[self._hptr + 8] + + @property + def proto(self) -> int: + """Read 'Protocol' field""" + + return self._frame[self._hptr + 9] + + @property + def cksum(self) -> int: + """Read 'Checksum' field""" + + if "_cache__cksum" not in self.__dict__: + self._cache__cksum = struct.unpack_from("!H", self._frame, self._hptr + 10)[0] + return self._cache__cksum + + @property + def src(self) -> IPv4Address: + """Read 'Source address' field""" + + if "_cache__src" not in self.__dict__: + self._cache__src = IPv4Address(self._frame[self._hptr + 12 : self._hptr + 16]) + return self._cache__src + + @property + def dst(self) -> IPv4Address: + """Read 'Destination address' field""" + + if "_cache__dst" not in self.__dict__: + self._cache__dst = IPv4Address(self._frame[self._hptr + 16 : self._hptr + 20]) + return self._cache__dst + + @property + def options(self) -> list: + """Read list of options""" + + if "_cache__options" not in self.__dict__: + self._cache__options: list = [] + optr = self._hptr + ip4.ps.HEADER_LEN + + while optr < self._hptr + self.hlen: + if self._frame[optr] == ip4.ps.OPT_EOL: + self._cache__options.append(OptEol()) + break + if self._frame[optr] == ip4.ps.OPT_NOP: + self._cache__options.append(OptNop()) + optr += ip4.ps.OPT_NOP_LEN + continue + # typing: Had to put single mapping (0: lambda _, __: None) into dict to suppress typng error + self._cache__options.append({0: lambda _, __: None}.get(self._frame[optr], OptUnk)(self._frame, optr)) + optr += self._frame[optr + 1] + + return self._cache__options + + @property + def olen(self) -> int: + """Calculate options length""" + + if "_cache__olen" not in self.__dict__: + self._cache__olen = self.hlen - ip4.ps.HEADER_LEN + return self._cache__olen + + @property + def dlen(self) -> int: + """Calculate data length""" + + if "_cache__dlen" not in self.__dict__: + self._cache__dlen = self.plen - self.hlen + return self._cache__dlen + + @property + def header_copy(self) -> bytes: + """Return copy of packet header""" + + if "_cache__header_copy" not in self.__dict__: + self._cache__header_copy = self._frame[self._hptr : self._hptr + ip4.ps.HEADER_LEN] + return self._cache__header_copy + + @property + def options_copy(self) -> bytes: + """Return copy of packet header""" + + if "_cache__options_copy" not in self.__dict__: + self._cache__options_copy = self._frame[self._hptr + ip4.ps.HEADER_LEN : self._hptr + self.hlen] + return self._cache__options_copy + + @property + def data_copy(self) -> bytes: + """Return copy of packet data""" + + if "_cache__data_copy" not in self.__dict__: + self._cache__data_copy = self._frame[self._hptr + self.hlen : self._hptr + self.plen] + return self._cache__data_copy + + @property + def packet_copy(self) -> bytes: + """Return copy of whole packet""" + + if "_cache__packet_copy" not in self.__dict__: + self._cache__packet_copy = self._frame[self._hptr : self._hptr + self.plen] + return self._cache__packet_copy + + @property + def pshdr_sum(self) -> int: + """Create IPv4 pseudo header used by TCP and UDP to compute their checksums""" + + if "_cache.__pshdr_sum" not in self.__dict__: + pseudo_header = struct.pack("! 4s 4s BBH", self.src.packed, self.dst.packed, 0, self.proto, self.plen - self.hlen) + self._cache__pshdr_sum = sum(struct.unpack("! 3L", pseudo_header)) + return self._cache__pshdr_sum + + def _packet_integrity_check(self) -> str: + """Packet integrity check to be run on raw packet prior to parsing to make sure parsing is safe""" + + if not config.packet_integrity_check: + return "" + + if len(self) < ip4.ps.HEADER_LEN: + return "IPv4 integrity - wrong packet length (I)" + + if not ip4.ps.HEADER_LEN <= self.hlen <= self.plen <= len(self): + return "IPv4 integrity - wrong packet length (II)" + + # Cannot compute checksum earlier because it depends on sanity of hlen field + if inet_cksum(self._frame, self._hptr, self.hlen): + return "IPv4 integriy - wrong packet checksum" + + optr = self._hptr + ip4.ps.HEADER_LEN + while optr < self._hptr + self.hlen: + if self._frame[optr] == ip4.ps.OPT_EOL: + break + if self._frame[optr] == ip4.ps.OPT_NOP: + optr += 1 + if optr > self._hptr + self.hlen: + return "IPv4 integrity - wrong option length (I)" + continue + if optr + 1 > self._hptr + self.hlen: + return "IPv4 integrity - wrong option length (II)" + if self._frame[optr + 1] == 0: + return "IPv4 integrity - wrong option length (III)" + optr += self._frame[optr + 1] + if optr > self._hptr + self.hlen: + return "IPv4 integrity - wrong option length (IV)" + + return "" + + def _packet_sanity_check(self) -> str: + """Packet sanity check to be run on parsed packet to make sure packet's fields contain sane values""" + + if not config.packet_sanity_check: + return "" + + if self.ver != 4: + return "IP sanityi - 'ver' must be 4" + + if self.ver == 0: + return "IP sanity - 'ttl' must be greater than 0" + + if self.src.is_multicast: + return "IP sanity - 'src' must not be multicast" + + if self.src.is_reserved: + return "IP sanity - 'src' must not be reserved" + + if self.src.is_limited_broadcast: + return "IP sanity - 'src' must not be limited broadcast" + + if self.flag_df and self.flag_mf: + return "IP sanity - 'flag_df' and 'flag_mf' must not be set simultaneously" + + if self.offset and self.flag_df: + return "IP sanity - 'offset' must be 0 when 'df_flag' is set" + + if self.options and config.ip4_option_packet_drop: + return "IP sanity - packet must not contain options" + + return "" + + +# +# IPv4 options +# + + +# IPv4 option - End of Option Linst + + +class OptEol(ip4.ps.OptEol): + """IPv4 option - End of Option List""" + + def __init__(self) -> None: + self.kind = ip4.ps.OPT_EOL + + +# IPv4 option - No Operation (1) + + +class OptNop(ip4.ps.OptNop): + """IPv4 option - No Operation""" + + def __init__(self) -> None: + self.kind = ip4.ps.OPT_NOP + + +# IPv4 option not supported by this stack + + +class OptUnk(ip4.ps.OptUnk): + """IPv4 option not supported by this stack""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.kind = frame[optr + 0] + self.len = frame[optr + 1] + self.data = frame[optr + 2 : optr + self.len] diff --git a/phrx_ip4.py b/ip4/phrx.py similarity index 73% rename from phrx_ip4.py rename to ip4/phrx.py index 3484f619..438f792b 100755 --- a/phrx_ip4.py +++ b/ip4/phrx.py @@ -23,36 +23,29 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phrx_ip4.py - packet handler for inbound IPv4 packets +# ip4/phrx.py - packet handler for inbound IPv4 packets # import struct from time import time +from typing import Optional, cast import config -import fpp_ip4 -from ip_helper import inet_cksum -from packet import PacketRx +import ip4.fpp +import ip4.ps +from ip4.fpp import Parser as Ip4Parser +from misc.ip_helper import inet_cksum +from misc.packet import PacketRx -def _defragment_ip4_packet(self, packet_rx): +def _defragment_ip4_packet(self, packet_rx: PacketRx) -> Optional[PacketRx]: """Defragment IPv4 packet""" + packet_rx.ip4 = cast(Ip4Parser, packet_rx.ip4) + # Cleanup expired flows self.ip4_frag_flows = { _: self.ip4_frag_flows[_] for _ in self.ip4_frag_flows if self.ip4_frag_flows[_]["timestamp"] - time() < config.ip4_frag_flow_timeout @@ -95,20 +88,20 @@ def _defragment_ip4_packet(self, packet_rx): struct.pack_into(f"{len(self.ip4_frag_flows[flow_id]['data'][offset])}s", data, offset, self.ip4_frag_flows[flow_id]["data"][offset]) del self.ip4_frag_flows[flow_id] header[0] = 0x45 - struct.pack_into("!H", header, 2, fpp_ip4.IP4_HEADER_LEN + len(data)) + struct.pack_into("!H", header, 2, ip4.ps.HEADER_LEN + len(data)) header[6] = header[7] = header[10] = header[11] = 0 - struct.pack_into("!H", header, 10, inet_cksum(header, 0, fpp_ip4.IP4_HEADER_LEN)) + struct.pack_into("!H", header, 10, inet_cksum(header, 0, ip4.ps.HEADER_LEN)) packet_rx = PacketRx(bytes(header) + data) - fpp_ip4.Ip4Packet(packet_rx) + ip4.fpp.Parser(packet_rx) if __debug__: self._logger.debug(f"{packet_rx.tracker} - Reasembled fragmented IPv4 packet, dlen {len(data)} bytes") return packet_rx -def _phrx_ip4(self, packet_rx): +def _phrx_ip4(self, packet_rx: PacketRx) -> None: """Handle inbound IPv4 packets""" - fpp_ip4.Ip4Packet(packet_rx) + ip4.fpp.Parser(packet_rx) if packet_rx.parse_failed: if __debug__: @@ -118,6 +111,8 @@ def _phrx_ip4(self, packet_rx): if __debug__: self._logger.debug(f"{packet_rx.tracker} - {packet_rx.ip4}") + packet_rx.ip4 = cast(Ip4Parser, packet_rx.ip4) + # Check if received packet has been sent to us directly or by unicast/broadcast, allow any destination if no unicast address is configured (for DHCP client) if self.ip4_unicast and packet_rx.ip4.dst not in {*self.ip4_unicast, *self.ip4_multicast, *self.ip4_broadcast}: if __debug__: @@ -126,17 +121,19 @@ def _phrx_ip4(self, packet_rx): # Check if packet is a fragment and if so process it accordingly if packet_rx.ip4.offset != 0 or packet_rx.ip4.flag_mf: - if not (packet_rx := _defragment_ip4_packet(self, packet_rx)): + if not (packet_rx := self._defragment_ip4_packet(packet_rx)): return - if packet_rx.ip4.proto == fpp_ip4.IP4_PROTO_ICMP4: + packet_rx.ip4 = cast(Ip4Parser, packet_rx.ip4) + + if packet_rx.ip4.proto == ip4.ps.PROTO_ICMP4: self._phrx_icmp4(packet_rx) return - if packet_rx.ip4.proto == fpp_ip4.IP4_PROTO_UDP: + if packet_rx.ip4.proto == ip4.ps.PROTO_UDP: self._phrx_udp(packet_rx) return - if packet_rx.ip4.proto == fpp_ip4.IP4_PROTO_TCP: + if packet_rx.ip4.proto == ip4.ps.PROTO_TCP: self._phrx_tcp(packet_rx) return diff --git a/phtx_ip4.py b/ip4/phtx.py similarity index 74% rename from phtx_ip4.py rename to ip4/phtx.py index ce01b18f..084855cb 100755 --- a/phtx_ip4.py +++ b/ip4/phtx.py @@ -23,30 +23,23 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phtx_ip4.py - packet handler for outbound IPv4 packets +# ip4/phtx.py - packet handler for outbound IPv4 packets # +from typing import Optional, Union + import config -import fpa_ip4 -from ipv4_address import IPv4Address +import icmp4.fpa +import ip4.fpa +import tcp.fpa +import udp.fpa +from misc.ipv4_address import IPv4Address -def validate_src_ip4_address(self, ip4_src, ip4_dst): +def _validate_src_ip4_address(self, ip4_src: IPv4Address, ip4_dst: IPv4Address) -> Optional[IPv4Address]: """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) @@ -79,9 +72,9 @@ def validate_src_ip4_address(self, ip4_src, ip4_dst): # 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 = [_.ip for _ in self.ip4_address if _.broadcast_address == ip4_src] - if ip4_src: - ip4_src = ip4_src[0] + ip4_src_list = [_.ip for _ in self.ip4_address if _.broadcast_address == ip4_src] + if ip4_src_list: + ip4_src = ip4_src_list[0] if __debug__: self._logger.debug(f"Packet is response to directed broadcast, replaced source with appropriate IPv4 address {ip4_src}") else: @@ -104,7 +97,7 @@ def validate_src_ip4_address(self, ip4_src, ip4_dst): return ip4_src -def validate_dst_ip4_address(self, ip4_dst): +def _validate_dst_ip4_address(self, ip4_dst: IPv4Address) -> Optional[IPv4Address]: """Make sure destination ip address is valid""" # Drop packet if the destination address is unspecified @@ -116,48 +109,52 @@ def validate_dst_ip4_address(self, ip4_dst): return ip4_dst -def _phtx_ip4(self, child_packet, ip4_dst, ip4_src, ip4_ttl=config.ip4_default_ttl): +def _phtx_ip4( + self, + carried_packet: Union[icmp4.fpa.Assembler, tcp.fpa.Assembler, udp.fpa.Assembler], + ip4_dst: IPv4Address, + ip4_src: IPv4Address, + ip4_ttl: int = config.ip4_default_ttl, +) -> None: """Handle outbound IP packets""" + assert 0 < ip4_ttl < 256 + # Check if IPv4 protocol support is enabled, if not then silently drop the packet if not config.ip4_support: return - # Make sure source and destination addresses are the right object type - ip4_src = IPv4Address(ip4_src) - ip4_dst = IPv4Address(ip4_dst) - # Validate source address - ip4_src = validate_src_ip4_address(self, ip4_src, ip4_dst) + ip4_src = self._validate_src_ip4_address(ip4_src, ip4_dst) if not ip4_src: return # Validate destination address - ip4_dst = validate_dst_ip4_address(self, ip4_dst) + ip4_dst = self._validate_dst_ip4_address(ip4_dst) if not ip4_dst: return # Assemble IPv4 packet - ip4_packet_tx = fpa_ip4.Ip4Packet(src=ip4_src, dst=ip4_dst, ttl=ip4_ttl, child_packet=child_packet) + ip4_packet_tx = ip4.fpa.Assembler(src=ip4_src, dst=ip4_dst, ttl=ip4_ttl, carried_packet=carried_packet) # Send packet out if it's size doesn't exceed mtu if len(ip4_packet_tx) <= config.mtu: if __debug__: self._logger.debug(f"{ip4_packet_tx.tracker} - {ip4_packet_tx}") - self._phtx_ether(child_packet=ip4_packet_tx) + self._phtx_ether(carried_packet=ip4_packet_tx) return # Fragment packet and send out if __debug__: self._logger.debug(f"{ip4_packet_tx.tracker} - IPv4 packet len {len(ip4_packet_tx)} bytes, fragmentation needed") data = bytearray(ip4_packet_tx.dlen) - ip4_packet_tx._child_packet.assemble_packet(data, 0, ip4_packet_tx.pshdr_sum) + ip4_packet_tx._carried_packet.assemble(data, 0, ip4_packet_tx.pshdr_sum) data_mtu = (config.mtu - ip4_packet_tx.hlen) & 0b1111111111111000 data_frags = [data[_ : data_mtu + _] for _ in range(0, len(data), data_mtu)] offset = 0 self.ip4_id += 1 for data_frag in data_frags: - ip4_frag_tx = fpa_ip4.Ip4Frag( + ip4_frag_tx = ip4.fpa.FragAssembler( src=ip4_src, dst=ip4_dst, ttl=ip4_ttl, @@ -170,5 +167,5 @@ def _phtx_ip4(self, child_packet, ip4_dst, ip4_src, ip4_ttl=config.ip4_default_t if __debug__: self._logger.debug(f"{ip4_frag_tx.tracker} - {ip4_frag_tx}") offset += len(data_frag) - self._phtx_ether(child_packet=ip4_frag_tx) + self._phtx_ether(carried_packet=ip4_frag_tx) return diff --git a/ip4/ps.py b/ip4/ps.py new file mode 100755 index 00000000..3fe9cdb2 --- /dev/null +++ b/ip4/ps.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ip4/ps.py - protocol support for IPv4 +# + + +# IPv4 protocol header + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# |Version| IHL | DSCP |ECN| Packet length | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Identification |Flags| Fragment offset | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Time to live | Protocol | Header checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Source address | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Destination address | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# ~ Options ~ Padding ~ +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +HEADER_LEN = 20 + +PROTO_ICMP4 = 1 +PROTO_TCP = 6 +PROTO_UDP = 17 + +PROTO_TABLE = {PROTO_ICMP4: "ICMPv4", PROTO_TCP: "TCP", PROTO_UDP: "UDP"} + + +def __str__(self) -> str: + """Packet log string""" + + return ( + f"IPv4 {self.src} > {self.dst}, proto {self.proto} ({PROTO_TABLE.get(self.proto, '???')}), id {self.id}" + + f"{', DF' if self.flag_df else ''}{', MF' if self.flag_mf else ''}, offset {self.offset}, plen {self.plen}" + + f", ttl {self.ttl}" + ) + + +# +# IPv4 options +# + + +# IPv4 option - End of Option Linst + +OPT_EOL = 0 +OPT_EOL_LEN = 1 + + +class OptEol: + """IPv4 option - End of Option List""" + + def __str__(self) -> str: + """Option log string""" + + return "eol" + + def __len__(self) -> int: + """Option length""" + + return OPT_EOL_LEN + + +# IPv4 option - No Operation (1) + +OPT_NOP = 1 +OPT_NOP_LEN = 1 + + +class OptNop: + """IPv4 option - No Operation""" + + def __str__(self) -> str: + """Option log string""" + + return "nop" + + def __len__(self) -> int: + """Option length""" + + return OPT_NOP_LEN + + +# IPv4 option not supported by this stack + + +class OptUnk: + """IPv4 option not supported by this stack""" + + def __init__(self) -> None: + """Class constructor""" + + self.kind = -1 + self.len = -1 + + def __str__(self) -> str: + """Option log string""" + + return f"unk-{self.kind}-{self.len}" diff --git a/ip6/__init__.py b/ip6/__init__.py new file mode 100755 index 00000000..3eb78127 --- /dev/null +++ b/ip6/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ip6/__init__.py +# diff --git a/ip6/fpa.py b/ip6/fpa.py new file mode 100755 index 00000000..e9356d6e --- /dev/null +++ b/ip6/fpa.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ip6/fpa.py - Fast Packet Assembler support class for IPv6 protocol +# + + +import struct +from typing import Union + +import config +import ether.ps +import icmp6.fpa +import ip6.ps +import ip6_ext_frag.fpa +import tcp.fpa +import udp.fpa +from misc.ipv6_address import IPv6Address + + +class Assembler: + """IPv6 packet assembler support class""" + + ether_type = ether.ps.TYPE_IP6 + + def __init__( + self, + carried_packet: Union[ip6_ext_frag.fpa.Assembler, icmp6.fpa.Assembler, tcp.fpa.Assembler, udp.fpa.Assembler], + src: IPv6Address, + dst: IPv6Address, + hop: int = config.ip6_default_hop, + dscp: int = 0, + ecn: int = 0, + flow: int = 0, + ) -> None: + """Class constructor""" + + assert carried_packet.ip6_next in {ip6.ps.NEXT_HEADER_ICMP6, ip6.ps.NEXT_HEADER_UDP, ip6.ps.NEXT_HEADER_TCP, ip6.ps.NEXT_HEADER_EXT_FRAG} + + self._carried_packet = carried_packet + self.tracker = self._carried_packet.tracker + self.ver = 6 + self.dscp = dscp + self.ecn = ecn + self.flow = flow + self.hop = hop + self.src = IPv6Address(src) + self.dst = IPv6Address(dst) + self.next = self._carried_packet.ip6_next + self.dlen = len(carried_packet) + + def __len__(self) -> int: + """Length of the packet""" + + return ip6.ps.HEADER_LEN + len(self._carried_packet) + + from ip6.ps import __str__ + + @property + def pshdr_sum(self) -> int: + """Returns IPv6 pseudo header that is used by TCP, UDP and ICMPv6 to compute their checksums""" + + pseudo_header = struct.pack("! 16s 16s L BBBB", self.src.packed, self.dst.packed, self.dlen, 0, 0, 0, self.next) + return sum(struct.unpack("! 5Q", pseudo_header)) + + def assemble(self, frame: bytearray, hptr: int) -> None: + """Assemble packet into the raw form""" + + struct.pack_into( + "! BBBB HBB 16s 16s", + frame, + hptr, + self.ver << 4 | self.dscp >> 4, + self.dscp << 6 | self.ecn << 4 | ((self.flow & 0b000011110000000000000000) >> 16), + (self.flow & 0b000000001111111100000000) >> 8, + self.flow & 0b000000000000000011111111, + self.dlen, + self.next, + self.hop, + self.src.packed, + self.dst.packed, + ) + + self._carried_packet.assemble(frame, hptr + ip6.ps.HEADER_LEN, self.pshdr_sum) diff --git a/ip6/fpp.py b/ip6/fpp.py new file mode 100755 index 00000000..63119eba --- /dev/null +++ b/ip6/fpp.py @@ -0,0 +1,206 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ip6.py/fpp - Fast Packet Parser support class for IPv6 protocol +# + + +import struct + +import config +import ip6.ps +from misc.ipv6_address import IPv6Address +from misc.packet import PacketRx + + +class Parser: + """IPv6 packet parser class""" + + def __init__(self, packet_rx: PacketRx) -> None: + """Class constructor""" + + packet_rx.ip6 = self + packet_rx.ip = self + + self._frame = packet_rx.frame + self._hptr = packet_rx.hptr + + packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() + + if not packet_rx.parse_failed: + packet_rx.hptr = self._hptr + ip6.ps.HEADER_LEN + + def __len__(self) -> int: + """Number of bytes remaining in the frame""" + + return len(self._frame) - self._hptr + + from ip6.ps import __str__ + + @property + def ver(self) -> int: + """Read 'Version' field""" + + if "_cache__ver" not in self.__dict__: + self._cache__ver = self._frame[self._hptr + 0] >> 4 + return self._cache__ver + + @property + def dscp(self) -> int: + """Read 'DSCP' field""" + + if "_cache__dscp" not in self.__dict__: + self._cache__dscp = ((self._frame[self._hptr + 0] & 0b00001111) << 2) | ((self._frame[self._hptr + 1] & 0b11000000) >> 6) + return self._cache__dscp + + @property + def ecn(self) -> int: + """Read 'ECN' field""" + + if "_cache__ecn" not in self.__dict__: + self._cache__ecn = (self._frame[self._hptr + 1] & 0b00110000) >> 4 + return self._cache__ecn + + @property + def flow(self) -> int: + """Read 'Flow' field""" + + if "_cache__flow" not in self.__dict__: + self._cache__flow = ((self._frame[self._hptr + 1] & 0b00001111) << 16) | (self._frame[self._hptr + 2] << 8) | self._frame[self._hptr + 3] + return self._cache__flow + + @property + def dlen(self) -> int: + """Read 'Data length' field""" + + if "_cache__dlen" not in self.__dict__: + self._cache__dlen = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] + return self._cache__dlen + + @property + def next(self) -> int: + """Read 'Next' field""" + + return self._frame[self._hptr + 6] + + @property + def hop(self) -> int: + """Read 'Hop' field""" + + return self._frame[self._hptr + 7] + + @property + def src(self) -> IPv6Address: + """Read 'Source address' field""" + + if "_cache__src" not in self.__dict__: + self._cache__src = IPv6Address(self._frame[self._hptr + 8 : self._hptr + 24]) + return self._cache__src + + @property + def dst(self) -> IPv6Address: + """Read 'Destination address' field""" + + if "_cache__dst" not in self.__dict__: + self._cache__dst = IPv6Address(self._frame[self._hptr + 24 : self._hptr + 40]) + return self._cache__dst + + @property + def hlen(self) -> int: + """Calculate header length""" + + return ip6.ps.HEADER_LEN + + @property + def plen(self) -> int: + """Calculate packet length""" + + return ip6.ps.HEADER_LEN + self.dlen + + @property + def header_copy(self) -> bytes: + """Return copy of packet header""" + + if "_cache__header_copy" not in self.__dict__: + self._cache__header_copy = self._frame[self._hptr : self._hptr + ip6.ps.HEADER_LEN] + return self._cache__header_copy + + @property + def data_copy(self) -> bytes: + """Return copy of packet data""" + + if "_cache__data_copy" not in self.__dict__: + self._cache__data_copy = self._frame[self._hptr + ip6.ps.HEADER_LEN : self._hptr + self.plen] + return self._cache__data_copy + + @property + def packet_copy(self) -> bytes: + """Return copy of whole packet""" + + if "_cache__packet_copy" not in self.__dict__: + self._cache__packet_copy = self._frame[self._hptr : self._hptr + self.plen] + return self._cache__packet_copy + + @property + def pshdr_sum(self) -> int: + """Returns IPv6 pseudo header that is used by TCP, UDP and ICMPv6 to compute their checksums""" + + if "_cache__pshdr_sum" not in self.__dict__: + pseudo_header = struct.pack("! 16s 16s L BBBB", self.src.packed, self.dst.packed, self.dlen, 0, 0, 0, self.next) + self._cache__pshdr_sum = sum(struct.unpack("! 5Q", pseudo_header)) + return self._cache__pshdr_sum + + def _packet_integrity_check(self) -> str: + """Packet integrity check to be run on raw packet prior to parsing to make sure parsing is safe""" + + if not config.packet_integrity_check: + return "" + + if len(self) < ip6.ps.HEADER_LEN: + return "IPv6 integrity - wrong packet length (I)" + + if struct.unpack_from("!H", self._frame, self._hptr + 4)[0] != len(self) - ip6.ps.HEADER_LEN: + return "IPv6 integrity - wrong packet length (II)" + + return "" + + def _packet_sanity_check(self) -> str: + """Packet sanity check to be run on parsed packet to make sure packet's fields contain sane values""" + + if not config.packet_sanity_check: + return "" + + if self.ver != 6: + return "IPv6 sanity - 'ver' must be 6" + + if self.hop == 0: + return "IPv6 sanity - 'hop' must not be 0" + + if self.src.is_multicast: + return "IPv6 sanity - 'src' must not be multicast" + + return "" diff --git a/phrx_ip6.py b/ip6/phrx.py similarity index 64% rename from phrx_ip6.py rename to ip6/phrx.py index b0b91436..aed220b5 100755 --- a/phrx_ip6.py +++ b/ip6/phrx.py @@ -23,31 +23,24 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phrx_ip6.py - packet handler for inbound IPv6 packets +# ip6/phrx.py - packet handler for inbound IPv6 packets # -import fpp_ip6 +from typing import cast + +import ip6.fpp +import ip6.ps +from ip6.fpp import Parser as Ip6Parser +from misc.packet import PacketRx -def _phrx_ip6(self, packet_rx): +def _phrx_ip6(self, packet_rx: PacketRx) -> None: """Handle inbound IPv6 packets""" - fpp_ip6.Ip6Packet(packet_rx) + ip6.fpp.Parser(packet_rx) if packet_rx.parse_failed: if __debug__: @@ -57,24 +50,26 @@ def _phrx_ip6(self, packet_rx): if __debug__: self._logger.debug(f"{packet_rx.tracker} - {packet_rx.ip6}") + packet_rx.ip6 = cast(Ip6Parser, packet_rx.ip6) + # Check if received packet has been sent to us directly or by unicast or multicast if packet_rx.ip6.dst not in {*self.ip6_unicast, *self.ip6_multicast}: if __debug__: self._logger.debug(f"{packet_rx.tracker} - IP packet not destined for this stack, dropping...") return - if packet_rx.ip6.next == fpp_ip6.IP6_NEXT_HEADER_EXT_FRAG: + if packet_rx.ip6.next == ip6.ps.NEXT_HEADER_EXT_FRAG: self._phrx_ip6_ext_frag(packet_rx) return - if packet_rx.ip6.next == fpp_ip6.IP6_NEXT_HEADER_ICMP6: + if packet_rx.ip6.next == ip6.ps.NEXT_HEADER_ICMP6: self._phrx_icmp6(packet_rx) return - if packet_rx.ip6.next == fpp_ip6.IP6_NEXT_HEADER_UDP: + if packet_rx.ip6.next == ip6.ps.NEXT_HEADER_UDP: self._phrx_udp(packet_rx) return - if packet_rx.ip6.next == fpp_ip6.IP6_NEXT_HEADER_TCP: + if packet_rx.ip6.next == ip6.ps.NEXT_HEADER_TCP: self._phrx_tcp(packet_rx) return diff --git a/phtx_ip6.py b/ip6/phtx.py similarity index 71% rename from phtx_ip6.py rename to ip6/phtx.py index 53d84fbc..dbe00e99 100755 --- a/phtx_ip6.py +++ b/ip6/phtx.py @@ -23,30 +23,24 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phtx_ip6.py - packet handler for outbound IPv6 packets +# ip6/phtx.py - packet handler for outbound IPv6 packets # +from typing import Optional, Union + import config -import fpa_ip6 -from ipv6_address import IPv6Address +import icmp6.fpa +import ip6.fpa +import ip6_ext_frag.fpa +import tcp.fpa +import udp.fpa +from misc.ipv6_address import IPv6Address -def validate_src_ip6_address(self, ip6_src, ip6_dst): +def _validate_src_ip6_address(self, ip6_src: IPv6Address, ip6_dst: IPv6Address) -> Optional[IPv6Address]: """Make sure source ip address is valid, supplement with valid one as appropriate""" # Check if the the source IP address belongs to this stack or its unspecified @@ -81,7 +75,7 @@ def validate_src_ip6_address(self, ip6_src, ip6_dst): return ip6_src -def validate_dst_ip6_address(self, ip6_dst): +def _validate_dst_ip6_address(self, ip6_dst: IPv6Address) -> Optional[IPv6Address]: """Make sure destination ip address is valid""" # Drop packet if the destination address is unspecified @@ -93,35 +87,39 @@ def validate_dst_ip6_address(self, ip6_dst): return ip6_dst -def _phtx_ip6(self, child_packet, ip6_dst, ip6_src, ip6_hop=config.ip6_default_hop): +def _phtx_ip6( + self, + carried_packet: Union[ip6_ext_frag.fpa.Assembler, icmp6.fpa.Assembler, tcp.fpa.Assembler, udp.fpa.Assembler], + ip6_dst: IPv6Address, + ip6_src: IPv6Address, + ip6_hop: int = config.ip6_default_hop, +) -> None: """Handle outbound IP packets""" + assert 0 < ip6_hop < 256 + # Check if IPv6 protocol support is enabled, if not then silently drop the packet if not config.ip6_support: return - # Make sure source and destination addresses are the right object type - ip6_src = IPv6Address(ip6_src) - ip6_dst = IPv6Address(ip6_dst) - # Validate source address - ip6_src = validate_src_ip6_address(self, ip6_src, ip6_dst) + ip6_src = self._validate_src_ip6_address(ip6_src, ip6_dst) if not ip6_src: return # Validate destination address - ip6_dst = validate_dst_ip6_address(self, ip6_dst) + ip6_dst = self._validate_dst_ip6_address(ip6_dst) if not ip6_dst: return # assemble IPv6 apcket - ip6_packet_tx = fpa_ip6.Ip6Packet(src=ip6_src, dst=ip6_dst, hop=ip6_hop, child_packet=child_packet) + ip6_packet_tx = ip6.fpa.Assembler(src=ip6_src, dst=ip6_dst, hop=ip6_hop, carried_packet=carried_packet) # Check if IP packet can be sent out without fragmentation, if so send it out if len(ip6_packet_tx) <= config.mtu: if __debug__: self._logger.debug(f"{ip6_packet_tx.tracker} - {ip6_packet_tx}") - self._phtx_ether(child_packet=ip6_packet_tx) + self._phtx_ether(carried_packet=ip6_packet_tx) return # Fragment packet and send out diff --git a/ip6/ps.py b/ip6/ps.py new file mode 100755 index 00000000..d92e9304 --- /dev/null +++ b/ip6/ps.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ip6/ps.py - protocol support for IPv6 +# + + +# IPv6 protocol header + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# |Version| Traffic Class | Flow Label | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Payload Length | Next Header | Hop Limit | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | > +# + + +# > > +# + Source Address + +# > > +# + + +# > | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | > +# + + +# > > +# + Destination Address + +# > > +# + + +# > | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +HEADER_LEN = 40 + +NEXT_HEADER_TCP = 6 +NEXT_HEADER_UDP = 17 +NEXT_HEADER_EXT_FRAG = 44 +NEXT_HEADER_ICMP6 = 58 + +NEXT_HEADER_TABLE = {NEXT_HEADER_TCP: "TCP", NEXT_HEADER_UDP: "UDP", NEXT_HEADER_EXT_FRAG: "FRAG", NEXT_HEADER_ICMP6: "ICMPv6"} + + +def __str__(self) -> str: + """Packet log string""" + + return ( + f"IPv6 {self.src} > {self.dst}, next {self.next} ({NEXT_HEADER_TABLE.get(self.next, '???')}), flow {self.flow}" + f", dlen {self.dlen}, hop {self.hop}" + ) diff --git a/ip6_ext_frag/__init__.py b/ip6_ext_frag/__init__.py new file mode 100755 index 00000000..1603b34c --- /dev/null +++ b/ip6_ext_frag/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ip6_ext_frag/__init__.py +# diff --git a/fpa_ip6_ext_frag.py b/ip6_ext_frag/fpa.py similarity index 51% rename from fpa_ip6_ext_frag.py rename to ip6_ext_frag/fpa.py index 3df2e6ca..bacc4620 100755 --- a/fpa_ip6_ext_frag.py +++ b/ip6_ext_frag/fpa.py @@ -23,85 +23,53 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# fpa_ip6_ext_frag.py - Fast Packet Assembler support class for IPv6 fragment extension header +# ip6_ext_frag/fpa.py - Fast Packet Assembler support class for IPv6 fragment extension header # import struct -from tracker import Tracker - -# IPv6 protocol fragmentation extension header - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Next header | Reserved | Offset |R|R|M| -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - -IP6_EXT_FRAG_LEN = 8 +import ip6.ps +import ip6_ext_frag.ps +from misc.tracker import Tracker -IP6_NEXT_HEADER_TCP = 6 -IP6_NEXT_HEADER_UDP = 17 -IP6_NEXT_HEADER_ICMP6 = 58 -IP6_NEXT_HEADER_TABLE = {IP6_NEXT_HEADER_TCP: "TCP", IP6_NEXT_HEADER_UDP: "UDP", IP6_NEXT_HEADER_ICMP6: "ICMPv6"} +class Assembler: + """IPv6 fragment extension header assembler support class""" - -class Ip6ExtFrag: - """IPv6 fragment extension header support class""" - - protocol = "IP6_EXT_FRAG" + ip6_next = ip6.ps.NEXT_HEADER_EXT_FRAG def __init__( self, - next, - offset, - flag_mf, - id, - data, + next: int, + offset: int, + flag_mf: bool, + id: int, + data: bytes, ): """Class constructor""" - self.tracker = Tracker("TX") + assert next in {ip6.ps.NEXT_HEADER_ICMP6, ip6.ps.NEXT_HEADER_UDP, ip6.ps.NEXT_HEADER_TCP} + self.tracker = Tracker("TX") self.next = next self.offset = offset self.flag_mf = flag_mf self.id = id self.data = data - self.dlen = len(data) self.plen = len(self) - def __str__(self): - """Packet log string""" - - return ( - f"IPv6_FRAG id {self.id}{', MF' if self.flag_mf else ''}, offset {self.offset}" - + f", next {self.next} ({IP6_NEXT_HEADER_TABLE.get(self.next, '???')})" - ) - - def __len__(self): + def __len__(self) -> int: """Length of the packet""" - return IP6_EXT_FRAG_LEN + len(self.data) + return ip6_ext_frag.ps.HEADER_LEN + len(self.data) + + from ip6_ext_frag.ps import __str__ - def assemble_packet(self, frame, hptr, _): + def assemble(self, frame: bytearray, hptr: int, _: int): """Assemble packet into the raw form""" struct.pack_into( diff --git a/fpp_ip6_ext_frag.py b/ip6_ext_frag/fpp.py similarity index 51% rename from fpp_ip6_ext_frag.py rename to ip6_ext_frag/fpp.py index 0799a806..30a76932 100755 --- a/fpp_ip6_ext_frag.py +++ b/ip6_ext_frag/fpp.py @@ -23,51 +23,20 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# fpp_ip6_ext_frag.py - packet parser IPv6 protocol fragmentation extension header +# ip6_ext_frag/fpp.py - Fast Packet Parser for IPv6 fragmentation extension header # import struct import config - -# IPv6 protocol fragmentation extension header - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Next header | Reserved | Offset |R|R|M| -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Id | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -IP6_EXT_FRAG_LEN = 8 - -IP6_NEXT_HEADER_TCP = 6 -IP6_NEXT_HEADER_UDP = 17 -IP6_NEXT_HEADER_ICMP6 = 58 - -IP6_NEXT_HEADER_TABLE = {IP6_NEXT_HEADER_TCP: "TCP", IP6_NEXT_HEADER_UDP: "UDP", IP6_NEXT_HEADER_ICMP6: "ICMPv6"} +import ip6_ext_frag.ps -class Ip6ExtFrag: - """IPv6 fragmentation extension headr support class""" - - class __not_cached: - pass +class Parser: + """IPv6 fragmentation extension header parser class""" def __init__(self, packet_rx): """Class constructor""" @@ -78,31 +47,18 @@ def __init__(self, packet_rx): self._hptr = packet_rx.hptr self._plen = packet_rx.ip6.dlen - self.__next = self.__not_cached - self.__offset = self.__not_cached - self.__id = self.__not_cached - self.__header_copy = self.__not_cached - self.__data_copy = self.__not_cached - self.__packet_copy = self.__not_cached - packet_rx.parse_failed = self._packet_integrity_check() or self._packet_sanity_check() if not packet_rx.parse_failed: - packet_rx.hptr = self._hptr + IP6_EXT_FRAG_LEN - - def __str__(self): - """Packet log string""" - - return ( - f"IPv6_FRAG id {self.id}{', MF' if self.flag_mf else ''}, offset {self.offset}" - + f", next {self.next} ({IP6_NEXT_HEADER_TABLE.get(self.next, '???')})" - ) + packet_rx.hptr = self._hptr + ip6_ext_frag.ps.HEADER_LEN def __len__(self): """Number of bytes remaining in the frame""" return len(self._frame) - self._hptr + from ip6_ext_frag.ps import __str__ + @property def next(self): """Read 'Next' field""" @@ -113,9 +69,9 @@ def next(self): def offset(self): """Read 'Fragment offset' field""" - if self.__offset is self.__not_cached: - self.__offset = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] & 0b1111111111111000 - return self.__offset + if "_cache__offset" not in self.__dict__: + self._cache__offset = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] & 0b1111111111111000 + return self._cache__offset @property def flag_mf(self): @@ -127,21 +83,21 @@ def flag_mf(self): def id(self): """Read 'Identification' field""" - if self.__id is self.__not_cached: - self.__id = struct.unpack_from("!L", self._frame, self._hptr + 4)[0] - return self.__id + if "_cache__id" not in self.__dict__: + self._cache__id = struct.unpack_from("!L", self._frame, self._hptr + 4)[0] + return self._cache__id @property def hlen(self): """Calculate header length""" - return IP6_EXT_FRAG_LEN + return ip6_ext_frag.ps.HEADER_LEN @property def dlen(self): """Calculate data length""" - return self._plen - IP6_EXT_FRAG_LEN + return self._plen - ip6_ext_frag.ps.HEADER_LEN @property def plen(self): @@ -153,25 +109,25 @@ def plen(self): def header_copy(self): """Return copy of packet header""" - if self.__header_copy is self.__not_cached: - self.__header_copy = self._frame[self._hptr : self._hptr + IP6_EXT_FRAG_LEN] - return self.__header_copy + if "_cache__header_copy" not in self.__dict__: + self._cache__header_copy = self._frame[self._hptr : self._hptr + ip6_ext_frag.ps.HEADER_LEN] + return self._cache__header_copy @property def data_copy(self): """Return copy of packet data""" - if self.__data_copy is self.__not_cached: - self.__data_copy = self._frame[self._hptr + IP6_EXT_FRAG_LEN : self._hptr + self.plen] - return self.__data_copy + if "_cache__data_copy" not in self.__dict__: + self._cache__data_copy = self._frame[self._hptr + ip6_ext_frag.ps.HEADER_LEN : self._hptr + self.plen] + return self._cache__data_copy @property def packet_copy(self): """Return copy of whole packet""" - if self.__packet_copy is self.__not_cached: - self.__packet_copy = self._frame[self._hptr : self._hptr + self.plen] - return self.__packet_copy + if "_cache__packet_copy" not in self.__dict__: + self._cache__packet_copy = self._frame[self._hptr : self._hptr + self.plen] + return self._cache__packet_copy def _packet_integrity_check(self): """Packet integrity check to be run on raw packet prior to parsing to make sure parsing is safe""" @@ -179,6 +135,9 @@ def _packet_integrity_check(self): if not config.packet_integrity_check: return False + if len(self) < ip6_ext_frag.ps.HEADER_LEN: + return "IPv4 integrity - wrong packet length (I)" + return False def _packet_sanity_check(self): diff --git a/phrx_ip6_ext_frag.py b/ip6_ext_frag/phrx.py similarity index 75% rename from phrx_ip6_ext_frag.py rename to ip6_ext_frag/phrx.py index e1f97b61..9ef79486 100755 --- a/phrx_ip6_ext_frag.py +++ b/ip6_ext_frag/phrx.py @@ -23,34 +23,27 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phrx_ip6_ext_frag.py - packet handler for inbound IPv6 fragment extension header +# ip6_ext_frag/phrx.py - packet handler for inbound IPv6 fragment extension header # import struct from time import time +from typing import Optional, cast import config -import fpp_ip6_ext_frag -from packet import PacketRx +import ip6_ext_frag.fpp +from ip6.fpp import Parser as Ip6Parser +from ip6_ext_frag.fpp import Parser as Ip6ExtFragParser +from misc.packet import PacketRx -def _defragment_ip6_packet(self, packet_rx): +def _defragment_ip6_packet(self, packet_rx: PacketRx) -> Optional[PacketRx]: """Defragment IPv6 packet""" + packet_rx.ip6_ext_frag = cast(Ip6ExtFragParser, packet_rx.ip6_ext_frag) + # Cleanup expired flows self.ip6_frag_flows = { _: self.ip6_frag_flows[_] for _ in self.ip6_frag_flows if self.ip6_frag_flows[_]["timestamp"] - time() < config.ip6_frag_flow_timeout @@ -62,6 +55,7 @@ def _defragment_ip6_packet(self, packet_rx): + f"{'' if packet_rx.ip6_ext_frag.flag_mf else ', last'}" ) + packet_rx.ip6 = cast(Ip6Parser, packet_rx.ip6) flow_id = (packet_rx.ip6.src, packet_rx.ip6.dst, packet_rx.ip6_ext_frag.id) # Update flow db @@ -100,10 +94,10 @@ def _defragment_ip6_packet(self, packet_rx): return packet_rx -def _phrx_ip6_ext_frag(self, packet_rx): +def _phrx_ip6_ext_frag(self, packet_rx: PacketRx) -> None: """Handle inbound IPv6 fragment extension header""" - fpp_ip6_ext_frag.Ip6ExtFrag(packet_rx) + ip6_ext_frag.fpp.Parser(packet_rx) if packet_rx.parse_failed: if __debug__: @@ -113,5 +107,5 @@ def _phrx_ip6_ext_frag(self, packet_rx): if __debug__: self._logger.debug(f"{packet_rx.tracker} - {packet_rx.ip6_ext_frag}") - if packet_rx := _defragment_ip6_packet(self, packet_rx): + if packet_rx := self._defragment_ip6_packet(packet_rx): self._phrx_ip6(packet_rx) diff --git a/phtx_ip6_ext_frag.py b/ip6_ext_frag/phtx.py similarity index 62% rename from phtx_ip6_ext_frag.py rename to ip6_ext_frag/phtx.py index a33d420b..cfd27814 100755 --- a/phtx_ip6_ext_frag.py +++ b/ip6_ext_frag/phtx.py @@ -23,30 +23,20 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phtx_ip6_ext_frag.py - packet handler for outbound IPv6 fragment extension header +# ip6_ext_frag/phtx.py - packet handler for outbound IPv6 fragment extension header # import config -import fpa_ip6 -import fpa_ip6_ext_frag +import ip6.fpa +import ip6.ps +import ip6_ext_frag.fpa +import ip6_ext_frag.ps -def _phtx_ip6_ext_frag(self, ip6_packet_tx): +def _phtx_ip6_ext_frag(self, ip6_packet_tx: ip6.fpa.Assembler) -> None: """Handle outbound IPv6 fagment extension header""" # Check if IPv6 protocol support is enabled, if not then silently drop the packet @@ -54,16 +44,16 @@ def _phtx_ip6_ext_frag(self, ip6_packet_tx): return data = bytearray(ip6_packet_tx.dlen) - ip6_packet_tx._child_packet.assemble_packet(data, 0, ip6_packet_tx.pshdr_sum) - data_mtu = (config.mtu - fpa_ip6.IP6_HEADER_LEN - fpa_ip6_ext_frag.IP6_EXT_FRAG_LEN) & 0b1111111111111000 + ip6_packet_tx._carried_packet.assemble(data, 0, ip6_packet_tx.pshdr_sum) + data_mtu = (config.mtu - ip6.ps.HEADER_LEN - ip6_ext_frag.ps.HEADER_LEN) & 0b1111111111111000 data_frags = [data[_ : data_mtu + _] for _ in range(0, len(data), data_mtu)] offset = 0 self.ip6_id += 1 for data_frag in data_frags: - ip6_ext_frag_tx = fpa_ip6_ext_frag.Ip6ExtFrag( + ip6_ext_frag_tx = ip6_ext_frag.fpa.Assembler( next=ip6_packet_tx.next, offset=offset, flag_mf=data_frag is not data_frags[-1], id=self.ip6_id, data=data_frag ) if __debug__: self._logger.debug(f"{ip6_ext_frag_tx.tracker} - {ip6_ext_frag_tx}") - self._phtx_ip6(ip6_src=ip6_packet_tx.src, ip6_dst=ip6_packet_tx.dst, child_packet=ip6_ext_frag_tx) + self._phtx_ip6(ip6_src=ip6_packet_tx.src, ip6_dst=ip6_packet_tx.dst, carried_packet=ip6_ext_frag_tx) offset += len(data_frag) diff --git a/ip6_ext_frag/ps.py b/ip6_ext_frag/ps.py new file mode 100755 index 00000000..75374cb3 --- /dev/null +++ b/ip6_ext_frag/ps.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# ip6_ext_frag/ps.py - protocol support for IPv6 fragmentation extension header +# + + +# IPv6 protocol fragmentation extension header + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Next header | Reserved | Offset |R|R|M| +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Id | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +HEADER_LEN = 8 + +NEXT_HEADER_TCP = 6 +NEXT_HEADER_UDP = 17 +NEXT_HEADER_ICMP6 = 58 + +NEXT_HEADER_TABLE = {NEXT_HEADER_TCP: "TCP", NEXT_HEADER_UDP: "UDP", NEXT_HEADER_ICMP6: "ICMPv6"} + + +def __str__(self): + """Packet log string""" + + return f"IPv6_FRAG id {self.id}{', MF' if self.flag_mf else ''}, offset {self.offset}" + f", next {self.next} ({NEXT_HEADER_TABLE.get(self.next, '???')})" diff --git a/lint.sh b/lint.sh index 555840c5..1c510cde 100755 --- a/lint.sh +++ b/lint.sh @@ -5,8 +5,10 @@ codespell -w --ignore-words-list="ect,ether,nd,tha" --quiet-level=2 ${PY_PATH} R echo '<<< ISORT' && \ isort --profile black ${PY_PATH} && \ echo '<<< BLACK' && \ -black ${PY_PATH} && \ +black -l 160 ${PY_PATH} && \ echo '<<< FLAKE8' && \ flake8 ${PY_PATH} && \ echo '<<< MYPY' && \ -mypy ${PY_PATH} +mypy ${PY_PATH} && \ +echo '<<< TESTSLIDE' && \ +testslide tests/test_*.py diff --git a/log b/log new file mode 100644 index 00000000..a20886d5 --- /dev/null +++ b/log @@ -0,0 +1,527 @@ +21-05-30 20:53:23 | DEBUG | cli_server.__thread_service: Stack CLI server started, bound to , port 777 +21-05-30 20:53:23 | DEBUG | timer.__init__: Started timer +21-05-30 20:53:23 | DEBUG | rx_ring.__init__: Started RX ring +21-05-30 20:53:23 | DEBUG | tx_ring.__init__: Started TX ring +21-05-30 20:53:23 | DEBUG | arp_cache.__init__: Started ARP cache +21-05-30 20:53:23 | DEBUG | icmp6_nd_cache.__init__: Started ICMPv6 Neighbor Discovery cache +21-05-30 20:53:23 | DEBUG | packet_handler.__init__: Started packet handler +21-05-30 20:53:23 | DEBUG | packet_handler._assign_ip6_multicast: Assigned IPv6 multicast ff02::1 +21-05-30 20:53:23 | DEBUG | packet_handler._assign_mac_multicast: Assigned MAC multicast 33:33:00:00:00:01 +21-05-30 20:53:23 | DEBUG | packet_handler._parse_stack_ip6_address_candidate: Parsing ('FE80::7/64', '') entry +21-05-30 20:53:23 | DEBUG | packet_handler._parse_stack_ip6_address_candidate: Parsed ('fe80::7/64', 'None') entry +21-05-30 20:53:23 | DEBUG | packet_handler._perform_ip6_nd_dad: ICMPv6 ND DAD - Starting process for fe80::7 +21-05-30 20:53:23 | DEBUG | packet_handler._assign_ip6_multicast: Assigned IPv6 multicast ff02::1:ff00:7 +21-05-30 20:53:23 | DEBUG | packet_handler._assign_mac_multicast: Assigned MAC multicast 33:33:ff:00:00:07 +21-05-30 20:53:23 | INFO | packet_handler._phtx_icmp6: TX0000 - ICMPv6 type 143, code 0 +21-05-30 20:53:23 | DEBUG | packet_handler._phtx_ip6: TX0000 - IPv6 :: > ff02::16, next 58 (ICMPv6), flow 0, dlen 28, hop 1 +21-05-30 20:53:23 | DEBUG | packet_handler._phtx_ether: TX0000 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:23 | DEBUG | packet_handler._phtx_ether: TX0000 - Resolved destination IPv6 ff02::16 to MAC 33:33:00:00:00:16 +21-05-30 20:53:23 | DEBUG | packet_handler._phtx_ether: TX0000 - ETHER 02:00:00:77:77:77 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:23 | DEBUG | tx_ring.enqueue: TX0000, queue len: 1 +21-05-30 20:53:23 | DEBUG | packet_handler._send_icmp6_multicast_listener_report: Sent out ICMPv6 Multicast Listener Report message for [IPv6Address('ff02::1:ff00:7')] +21-05-30 20:53:23 | INFO | packet_handler._phtx_icmp6: TX0001 - ICMPv6 type 135, code 0, target fe80::7 +21-05-30 20:53:23 | DEBUG | packet_handler._phtx_ip6: TX0001 - IPv6 :: > ff02::1:ff00:7, next 58 (ICMPv6), flow 0, dlen 24, hop 255 +21-05-30 20:53:23 | DEBUG | packet_handler._phtx_ether: TX0001 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:23 | DEBUG | packet_handler._phtx_ether: TX0001 - Resolved destination IPv6 ff02::1:ff00:7 to MAC 33:33:ff:00:00:07 +21-05-30 20:53:23 | DEBUG | packet_handler._phtx_ether: TX0001 - ETHER 02:00:00:77:77:77 > 33:33:ff:00:00:07, 0x86dd (IPv6) +21-05-30 20:53:23 | DEBUG | tx_ring.enqueue: TX0001, queue len: 2 +21-05-30 20:53:23 | DEBUG | packet_handler._send_icmp6_nd_dad_message: Sent out ICMPv6 ND DAD message for fe80::7 +21-05-30 20:53:23 | DEBUG | tx_ring.__thread_transmit: [TX] TX0000 - sent frame, 82 bytes +21-05-30 20:53:23 | DEBUG | tx_ring.__thread_transmit: [TX] TX0001 - sent frame, 78 bytes +21-05-30 20:53:23 | DEBUG | rx_ring.__thread_receive: [RX] RX0000> - received frame, 110 bytes +21-05-30 20:53:23 | DEBUG | packet_handler._phrx_ether: RX0000 - ETHER 6e:97:13:b5:c2:21 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:23 | DEBUG | packet_handler._phrx_ether: RX0000 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:23 | DEBUG | rx_ring.__thread_receive: [RX] RX0001> - received frame, 110 bytes +21-05-30 20:53:23 | DEBUG | packet_handler._phrx_ether: RX0001 - ETHER 6e:97:13:b5:c2:21 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:23 | DEBUG | packet_handler._phrx_ether: RX0001 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:24 | DEBUG | packet_handler._perform_ip6_nd_dad: ICMPv6 ND DAD - No duplicate address detected for fe80::7 +21-05-30 20:53:24 | DEBUG | packet_handler._remove_ip6_multicast: Removed IPv6 multicast ff02::1:ff00:7 +21-05-30 20:53:24 | DEBUG | packet_handler._remove_mac_multicast: Removed MAC multicast 33:33:ff:00:00:07 +21-05-30 20:53:24 | DEBUG | packet_handler._assign_ip6_address: Assigned IPv6 unicast address fe80::7/64 +21-05-30 20:53:24 | DEBUG | packet_handler._assign_ip6_multicast: Assigned IPv6 multicast ff02::1:ff00:7 +21-05-30 20:53:24 | DEBUG | packet_handler._assign_mac_multicast: Assigned MAC multicast 33:33:ff:00:00:07 +21-05-30 20:53:24 | INFO | packet_handler._phtx_icmp6: TX0002 - ICMPv6 type 143, code 0 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ip6: TX0002 - IPv6 fe80::7 > ff02::16, next 58 (ICMPv6), flow 0, dlen 28, hop 1 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ether: TX0002 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ether: TX0002 - Resolved destination IPv6 ff02::16 to MAC 33:33:00:00:00:16 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ether: TX0002 - ETHER 02:00:00:77:77:77 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:24 | DEBUG | tx_ring.enqueue: TX0002, queue len: 1 +21-05-30 20:53:24 | DEBUG | packet_handler._send_icmp6_multicast_listener_report: Sent out ICMPv6 Multicast Listener Report message for [IPv6Address('ff02::1:ff00:7')] +21-05-30 20:53:24 | DEBUG | packet_handler.__: Successfully claimed IPv6 address fe80::7/64 +21-05-30 20:53:24 | DEBUG | packet_handler._perform_ip6_nd_dad: ICMPv6 ND DAD - Starting process for fe80::ff:fe77:7777 +21-05-30 20:53:24 | DEBUG | packet_handler._assign_ip6_multicast: Assigned IPv6 multicast ff02::1:ff77:7777 +21-05-30 20:53:24 | DEBUG | packet_handler._assign_mac_multicast: Assigned MAC multicast 33:33:ff:77:77:77 +21-05-30 20:53:24 | INFO | packet_handler._phtx_icmp6: TX0003 - ICMPv6 type 143, code 0 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ip6: TX0003 - IPv6 fe80::7 > ff02::16, next 58 (ICMPv6), flow 0, dlen 48, hop 1 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ether: TX0003 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ether: TX0003 - Resolved destination IPv6 ff02::16 to MAC 33:33:00:00:00:16 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ether: TX0003 - ETHER 02:00:00:77:77:77 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:24 | DEBUG | tx_ring.enqueue: TX0003, queue len: 2 +21-05-30 20:53:24 | DEBUG | packet_handler._send_icmp6_multicast_listener_report: Sent out ICMPv6 Multicast Listener Report message for [IPv6Address('ff02::1:ff77:7777'), IPv6Address('ff02::1:ff00:7')] +21-05-30 20:53:24 | INFO | packet_handler._phtx_icmp6: TX0004 - ICMPv6 type 135, code 0, target fe80::ff:fe77:7777 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ip6: TX0004 - IPv6 :: > ff02::1:ff77:7777, next 58 (ICMPv6), flow 0, dlen 24, hop 255 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ether: TX0004 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ether: TX0004 - Resolved destination IPv6 ff02::1:ff77:7777 to MAC 33:33:ff:77:77:77 +21-05-30 20:53:24 | DEBUG | packet_handler._phtx_ether: TX0004 - ETHER 02:00:00:77:77:77 > 33:33:ff:77:77:77, 0x86dd (IPv6) +21-05-30 20:53:24 | DEBUG | tx_ring.enqueue: TX0004, queue len: 3 +21-05-30 20:53:24 | DEBUG | packet_handler._send_icmp6_nd_dad_message: Sent out ICMPv6 ND DAD message for fe80::ff:fe77:7777 +21-05-30 20:53:24 | DEBUG | tx_ring.__thread_transmit: [TX] TX0002 - sent frame, 82 bytes +21-05-30 20:53:24 | DEBUG | tx_ring.__thread_transmit: [TX] TX0003 - sent frame, 102 bytes +21-05-30 20:53:24 | DEBUG | tx_ring.__thread_transmit: [TX] TX0004 - sent frame, 78 bytes +21-05-30 20:53:24 | DEBUG | rx_ring.__thread_receive: [RX] RX0002> - received frame, 241 bytes +21-05-30 20:53:24 | DEBUG | rx_ring.__thread_receive: [RX] RX0003> - received frame, 261 bytes +21-05-30 20:53:24 | DEBUG | packet_handler._phrx_ether: RX0002 - ETHER 5e:b6:83:7c:dc:63 > 01:00:5e:00:00:fb, 0x0800 (IPv4) +21-05-30 20:53:24 | DEBUG | packet_handler._phrx_ether: RX0002 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:24 | DEBUG | packet_handler._phrx_ether: RX0003 - ETHER 5e:b6:83:7c:dc:63 > 33:33:00:00:00:fb, 0x86dd (IPv6) +21-05-30 20:53:24 | DEBUG | packet_handler._phrx_ether: RX0003 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:24 | DEBUG | rx_ring.__thread_receive: [RX] RX0004> - received frame, 216 bytes +21-05-30 20:53:24 | DEBUG | packet_handler._phrx_ether: RX0004 - ETHER 54:26:96:e1:b4:05 > 01:00:5e:7f:ff:fa, 0x0800 (IPv4) +21-05-30 20:53:24 | DEBUG | packet_handler._phrx_ether: RX0004 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:25 | DEBUG | packet_handler._perform_ip6_nd_dad: ICMPv6 ND DAD - No duplicate address detected for fe80::ff:fe77:7777 +21-05-30 20:53:25 | DEBUG | packet_handler._remove_ip6_multicast: Removed IPv6 multicast ff02::1:ff77:7777 +21-05-30 20:53:25 | DEBUG | packet_handler._remove_mac_multicast: Removed MAC multicast 33:33:ff:77:77:77 +21-05-30 20:53:25 | DEBUG | packet_handler._assign_ip6_address: Assigned IPv6 unicast address fe80::ff:fe77:7777/64 +21-05-30 20:53:25 | DEBUG | packet_handler._assign_ip6_multicast: Assigned IPv6 multicast ff02::1:ff77:7777 +21-05-30 20:53:25 | DEBUG | packet_handler._assign_mac_multicast: Assigned MAC multicast 33:33:ff:77:77:77 +21-05-30 20:53:25 | INFO | packet_handler._phtx_icmp6: TX0005 - ICMPv6 type 143, code 0 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ip6: TX0005 - IPv6 fe80::7 > ff02::16, next 58 (ICMPv6), flow 0, dlen 48, hop 1 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0005 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0005 - Resolved destination IPv6 ff02::16 to MAC 33:33:00:00:00:16 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0005 - ETHER 02:00:00:77:77:77 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:25 | DEBUG | tx_ring.enqueue: TX0005, queue len: 1 +21-05-30 20:53:25 | DEBUG | packet_handler._send_icmp6_multicast_listener_report: Sent out ICMPv6 Multicast Listener Report message for [IPv6Address('ff02::1:ff77:7777'), IPv6Address('ff02::1:ff00:7')] +21-05-30 20:53:25 | DEBUG | tx_ring.__thread_transmit: [TX] TX0005 - sent frame, 102 bytes +21-05-30 20:53:25 | DEBUG | packet_handler.__: Successfully claimed IPv6 address fe80::ff:fe77:7777/64 +21-05-30 20:53:25 | INFO | packet_handler._phtx_icmp6: TX0006 - ICMPv6 type 133, code 0, slla 02:00:00:77:77:77 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ip6: TX0006 - IPv6 fe80::7 > ff02::2, next 58 (ICMPv6), flow 0, dlen 16, hop 255 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0006 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0006 - Resolved destination IPv6 ff02::2 to MAC 33:33:00:00:00:02 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0006 - ETHER 02:00:00:77:77:77 > 33:33:00:00:00:02, 0x86dd (IPv6) +21-05-30 20:53:25 | DEBUG | tx_ring.enqueue: TX0006, queue len: 1 +21-05-30 20:53:25 | DEBUG | packet_handler._send_icmp6_nd_router_solicitation: Sent out ICMPv6 ND Router Solicitation +21-05-30 20:53:25 | DEBUG | tx_ring.__thread_transmit: [TX] TX0006 - sent frame, 70 bytes +21-05-30 20:53:25 | DEBUG | rx_ring.__thread_receive: [RX] RX0005> - received frame, 158 bytes +21-05-30 20:53:25 | DEBUG | packet_handler._phrx_ether: RX0005 - ETHER 8e:ac:c2:c8:d6:ea > 02:00:00:77:77:77, 0x86dd (IPv6) +21-05-30 20:53:25 | DEBUG | packet_handler._phrx_ip6: RX0005 - IPv6 fe80::8cac:c2ff:fec8:d6ea > fe80::7, next 58 (ICMPv6), flow 0, dlen 104, hop 255 +21-05-30 20:53:25 | INFO | packet_handler._phrx_icmp6: RX0005 - ICMPv6 type 134, code 0, hop 64flags MOrlft 1800, reacht 0, retrt 0, prefix_info 2603:9000:e307:9f09::/64, unk-25-24, unk-31-16, unk-5-8, slla 8e:ac:c2:c8:d6:ea +21-05-30 20:53:25 | DEBUG | packet_handler._phrx_icmp6: Received ICMPv6 Router Advertisement packet from fe80::8cac:c2ff:fec8:d6ea +21-05-30 20:53:25 | DEBUG | packet_handler._create_stack_ip6_addressing: Attempting IPv6 address auto configuration for RA prefix 2603:9000:e307:9f09::/64 +21-05-30 20:53:25 | DEBUG | packet_handler._perform_ip6_nd_dad: ICMPv6 ND DAD - Starting process for 2603:9000:e307:9f09:0:ff:fe77:7777 +21-05-30 20:53:25 | DEBUG | packet_handler._assign_ip6_multicast: Assigned IPv6 multicast ff02::1:ff77:7777 +21-05-30 20:53:25 | DEBUG | packet_handler._assign_mac_multicast: Assigned MAC multicast 33:33:ff:77:77:77 +21-05-30 20:53:25 | INFO | packet_handler._phtx_icmp6: TX0007 - ICMPv6 type 143, code 0 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ip6: TX0007 - IPv6 fe80::7 > ff02::16, next 58 (ICMPv6), flow 0, dlen 48, hop 1 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0007 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0007 - Resolved destination IPv6 ff02::16 to MAC 33:33:00:00:00:16 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0007 - ETHER 02:00:00:77:77:77 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:25 | DEBUG | tx_ring.enqueue: TX0007, queue len: 1 +21-05-30 20:53:25 | DEBUG | packet_handler._send_icmp6_multicast_listener_report: Sent out ICMPv6 Multicast Listener Report message for [IPv6Address('ff02::1:ff77:7777'), IPv6Address('ff02::1:ff00:7')] +21-05-30 20:53:25 | DEBUG | tx_ring.__thread_transmit: [TX] TX0007 - sent frame, 102 bytes +21-05-30 20:53:25 | INFO | packet_handler._phtx_icmp6: TX0008 - ICMPv6 type 135, code 0, target 2603:9000:e307:9f09:0:ff:fe77:7777 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ip6: TX0008 - IPv6 :: > ff02::1:ff77:7777, next 58 (ICMPv6), flow 0, dlen 24, hop 255 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0008 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0008 - Resolved destination IPv6 ff02::1:ff77:7777 to MAC 33:33:ff:77:77:77 +21-05-30 20:53:25 | DEBUG | packet_handler._phtx_ether: TX0008 - ETHER 02:00:00:77:77:77 > 33:33:ff:77:77:77, 0x86dd (IPv6) +21-05-30 20:53:25 | DEBUG | tx_ring.enqueue: TX0008, queue len: 1 +21-05-30 20:53:25 | DEBUG | packet_handler._send_icmp6_nd_dad_message: Sent out ICMPv6 ND DAD message for 2603:9000:e307:9f09:0:ff:fe77:7777 +21-05-30 20:53:25 | DEBUG | tx_ring.__thread_transmit: [TX] TX0008 - sent frame, 78 bytes +21-05-30 20:53:25 | DEBUG | rx_ring.__thread_receive: [RX] RX0006> - received frame, 216 bytes +21-05-30 20:53:25 | DEBUG | packet_handler._phrx_ether: RX0006 - ETHER 54:26:96:e1:b4:05 > 01:00:5e:7f:ff:fa, 0x0800 (IPv4) +21-05-30 20:53:25 | DEBUG | packet_handler._phrx_ether: RX0006 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:26 | DEBUG | packet_handler._perform_ip6_nd_dad: ICMPv6 ND DAD - No duplicate address detected for 2603:9000:e307:9f09:0:ff:fe77:7777 +21-05-30 20:53:26 | DEBUG | packet_handler._remove_ip6_multicast: Removed IPv6 multicast ff02::1:ff77:7777 +21-05-30 20:53:26 | DEBUG | packet_handler._remove_mac_multicast: Removed MAC multicast 33:33:ff:77:77:77 +21-05-30 20:53:26 | DEBUG | packet_handler._assign_ip6_address: Assigned IPv6 unicast address 2603:9000:e307:9f09:0:ff:fe77:7777/64 +21-05-30 20:53:26 | DEBUG | packet_handler._assign_ip6_multicast: Assigned IPv6 multicast ff02::1:ff77:7777 +21-05-30 20:53:26 | DEBUG | packet_handler._assign_mac_multicast: Assigned MAC multicast 33:33:ff:77:77:77 +21-05-30 20:53:26 | INFO | packet_handler._phtx_icmp6: TX0009 - ICMPv6 type 143, code 0 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ip6: TX0009 - IPv6 fe80::7 > ff02::16, next 58 (ICMPv6), flow 0, dlen 48, hop 1 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX0009 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX0009 - Resolved destination IPv6 ff02::16 to MAC 33:33:00:00:00:16 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX0009 - ETHER 02:00:00:77:77:77 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:26 | DEBUG | tx_ring.enqueue: TX0009, queue len: 1 +21-05-30 20:53:26 | DEBUG | packet_handler._send_icmp6_multicast_listener_report: Sent out ICMPv6 Multicast Listener Report message for [IPv6Address('ff02::1:ff77:7777'), IPv6Address('ff02::1:ff00:7')] +21-05-30 20:53:26 | DEBUG | tx_ring.__thread_transmit: [TX] TX0009 - sent frame, 102 bytes +21-05-30 20:53:26 | DEBUG | packet_handler.__: Successfully claimed IPv6 address 2603:9000:e307:9f09:0:ff:fe77:7777/64 +21-05-30 20:53:26 | DEBUG | socket.__init__: Opened UDP socket UDP/None/None/*/* +21-05-30 20:53:26 | DEBUG | socket.bind: UDP/0.0.0.0/68/*/* - Socket bound to local address +21-05-30 20:53:26 | INFO | packet_handler._phtx_udp: TX000A - UDP 68 > 67, len 274 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ip4: TX000A - IPv4 0.0.0.0 > 255.255.255.255, proto 17 (UDP), id 0, offset 0, plen 294, ttl 64 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000A - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000A - Resolved destination IPv4 255.255.255.255 to MAC ff:ff:ff:ff:ff:ff +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000A - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0800 (IPv4) +21-05-30 20:53:26 | DEBUG | tx_ring.enqueue: TX000A, queue len: 1 +21-05-30 20:53:26 | DEBUG | packet_handler._dhcp4_client: Sent out DHCP Discover message +21-05-30 20:53:26 | DEBUG | tx_ring.__thread_transmit: [TX] TX000A - sent frame, 308 bytes +21-05-30 20:53:26 | DEBUG | rx_ring.__thread_receive: [RX] RX0007> - received frame, 342 bytes +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX0007 - ETHER 8e:ac:c2:c8:d6:ea > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ip4: RX0007 - IPv4 192.168.9.1 > 192.168.9.53, proto 17 (UDP), id 0, offset 0, plen 328, ttl 128 +21-05-30 20:53:26 | INFO | packet_handler._phrx_udp: RX0007 - UDP 67 > 68, len 308 +21-05-30 20:53:26 | DEBUG | socket._phrx_udp: RX0007 - Found matching listening socket UDP/0.0.0.0/68/*/* +21-05-30 20:53:26 | DEBUG | packet_handler._dhcp4_client: ClientUdpDhcp: Received DHCP Offer from 192.168.9.1IP: 192.168.9.53, Mask: 255.255.255.0, Router: [IPv4Address('192.168.9.1')]DNS: [IPv4Address('192.168.9.1')], Domain: local +21-05-30 20:53:26 | INFO | packet_handler._phtx_udp: TX000B - UDP 68 > 67, len 286 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ip4: TX000B - IPv4 0.0.0.0 > 255.255.255.255, proto 17 (UDP), id 0, offset 0, plen 306, ttl 64 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000B - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000B - Resolved destination IPv4 255.255.255.255 to MAC ff:ff:ff:ff:ff:ff +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000B - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0800 (IPv4) +21-05-30 20:53:26 | DEBUG | tx_ring.enqueue: TX000B, queue len: 1 +21-05-30 20:53:26 | DEBUG | packet_handler._dhcp4_client: Sent out DHCP Request message to 192.168.9.1 +21-05-30 20:53:26 | DEBUG | tx_ring.__thread_transmit: [TX] TX000B - sent frame, 320 bytes +21-05-30 20:53:26 | DEBUG | rx_ring.__thread_receive: [RX] RX0008> - received frame, 342 bytes +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX0008 - ETHER 8e:ac:c2:c8:d6:ea > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ip4: RX0008 - IPv4 192.168.9.1 > 192.168.9.53, proto 17 (UDP), id 0, offset 0, plen 328, ttl 128 +21-05-30 20:53:26 | INFO | packet_handler._phrx_udp: RX0008 - UDP 67 > 68, len 308 +21-05-30 20:53:26 | DEBUG | socket._phrx_udp: RX0008 - Found matching listening socket UDP/0.0.0.0/68/*/* +21-05-30 20:53:26 | DEBUG | packet_handler._dhcp4_client: Received DHCP Offer from 192.168.9.1IP: 192.168.9.53, Mask: 255.255.255.0, Router: [IPv4Address('192.168.9.1')]DNS: [IPv4Address('192.168.9.1')], Domain: local +21-05-30 20:53:26 | DEBUG | socket.close: Closed UDP socket UDP/0.0.0.0/68/*/* +21-05-30 20:53:26 | DEBUG | packet_handler._parse_stack_ip4_address_candidate: Parsing ('192.168.9.7/24', '192.168.9.1') entry +21-05-30 20:53:26 | DEBUG | packet_handler._parse_stack_ip4_address_candidate: Parsed ('192.168.9.7/24', '192.168.9.1') entry +21-05-30 20:53:26 | DEBUG | packet_handler._parse_stack_ip4_address_candidate: Parsing ('192.168.9.53/24', '192.168.9.1') entry +21-05-30 20:53:26 | DEBUG | packet_handler._parse_stack_ip4_address_candidate: Parsed ('192.168.9.53/24', '192.168.9.1') entry +21-05-30 20:53:26 | INFO | packet_handler._phtx_arp: TX000C - ARP request 0.0.0.0 / 02:00:00:77:77:77 > 192.168.9.7 / 00:00:00:00:00:00 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000C - Contains valid destination MAC address +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000C - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0806 (ARP) +21-05-30 20:53:26 | DEBUG | tx_ring.enqueue: TX000C, queue len: 1 +21-05-30 20:53:26 | DEBUG | packet_handler._send_arp_probe: Sent out ARP probe for 192.168.9.7 +21-05-30 20:53:26 | DEBUG | tx_ring.__thread_transmit: [TX] TX000C - sent frame, 42 bytes +21-05-30 20:53:26 | DEBUG | packet_handler._create_stack_ip4_addressing: Sent out ARP Probe for 192.168.9.7 +21-05-30 20:53:26 | INFO | packet_handler._phtx_arp: TX000D - ARP request 0.0.0.0 / 02:00:00:77:77:77 > 192.168.9.53 / 00:00:00:00:00:00 +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000D - Contains valid destination MAC address +21-05-30 20:53:26 | DEBUG | packet_handler._phtx_ether: TX000D - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0806 (ARP) +21-05-30 20:53:26 | DEBUG | tx_ring.enqueue: TX000D, queue len: 1 +21-05-30 20:53:26 | DEBUG | packet_handler._send_arp_probe: Sent out ARP probe for 192.168.9.53 +21-05-30 20:53:26 | DEBUG | tx_ring.__thread_transmit: [TX] TX000D - sent frame, 42 bytes +21-05-30 20:53:26 | DEBUG | packet_handler._create_stack_ip4_addressing: Sent out ARP Probe for 192.168.9.53 +21-05-30 20:53:26 | DEBUG | rx_ring.__thread_receive: [RX] RX0009> - received frame, 154 bytes +21-05-30 20:53:26 | DEBUG | rx_ring.__thread_receive: [RX] RX000A> - received frame, 174 bytes +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX0009 - ETHER 5e:b6:83:7c:dc:63 > 01:00:5e:00:00:fb, 0x0800 (IPv4) +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX0009 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000A - ETHER 5e:b6:83:7c:dc:63 > 33:33:00:00:00:fb, 0x86dd (IPv6) +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000A - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:26 | DEBUG | rx_ring.__thread_receive: [RX] RX000B> - received frame, 216 bytes +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000B - ETHER 54:26:96:e1:b4:05 > 01:00:5e:7f:ff:fa, 0x0800 (IPv4) +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000B - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:26 | DEBUG | rx_ring.__thread_receive: [RX] RX000C> - received frame, 90 bytes +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000C - ETHER 5e:b6:83:7c:dc:63 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000C - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:26 | DEBUG | rx_ring.__thread_receive: [RX] RX000D> - received frame, 180 bytes +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000D - ETHER 0c:9d:92:85:17:c1 > 33:33:00:01:00:02, 0x86dd (IPv6) +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000D - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:26 | DEBUG | rx_ring.__thread_receive: [RX] RX000E> - received frame, 150 bytes +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000E - ETHER 0c:9d:92:85:17:c1 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:26 | DEBUG | packet_handler._phrx_ether: RX000E - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:27 | DEBUG | rx_ring.__thread_receive: [RX] RX000F> - received frame, 150 bytes +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX000F - ETHER 0c:9d:92:85:17:c1 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX000F - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:27 | DEBUG | rx_ring.__thread_receive: [RX] RX0010> - received frame, 54 bytes +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0010 - ETHER 5e:b6:83:7c:dc:63 > 01:00:5e:00:00:16, 0x0800 (IPv4) +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0010 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:27 | DEBUG | rx_ring.__thread_receive: [RX] RX0011> - received frame, 241 bytes +21-05-30 20:53:27 | DEBUG | rx_ring.__thread_receive: [RX] RX0012> - received frame, 261 bytes +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0011 - ETHER 5e:b6:83:7c:dc:63 > 01:00:5e:00:00:fb, 0x0800 (IPv4) +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0011 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0012 - ETHER 5e:b6:83:7c:dc:63 > 33:33:00:00:00:fb, 0x86dd (IPv6) +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0012 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:27 | DEBUG | rx_ring.__thread_receive: [RX] RX0013> - received frame, 216 bytes +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0013 - ETHER 54:26:96:e1:b4:05 > 01:00:5e:7f:ff:fa, 0x0800 (IPv4) +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0013 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:27 | DEBUG | rx_ring.__thread_receive: [RX] RX0014> - received frame, 90 bytes +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0014 - ETHER 5e:b6:83:7c:dc:63 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:27 | DEBUG | packet_handler._phrx_ether: RX0014 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:28 | INFO | packet_handler._phtx_arp: TX000E - ARP request 0.0.0.0 / 02:00:00:77:77:77 > 192.168.9.7 / 00:00:00:00:00:00 +21-05-30 20:53:28 | DEBUG | packet_handler._phtx_ether: TX000E - Contains valid destination MAC address +21-05-30 20:53:28 | DEBUG | packet_handler._phtx_ether: TX000E - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0806 (ARP) +21-05-30 20:53:28 | DEBUG | tx_ring.enqueue: TX000E, queue len: 1 +21-05-30 20:53:28 | DEBUG | packet_handler._send_arp_probe: Sent out ARP probe for 192.168.9.7 +21-05-30 20:53:28 | DEBUG | tx_ring.__thread_transmit: [TX] TX000E - sent frame, 42 bytes +21-05-30 20:53:28 | DEBUG | packet_handler._create_stack_ip4_addressing: Sent out ARP Probe for 192.168.9.7 +21-05-30 20:53:28 | INFO | packet_handler._phtx_arp: TX000F - ARP request 0.0.0.0 / 02:00:00:77:77:77 > 192.168.9.53 / 00:00:00:00:00:00 +21-05-30 20:53:28 | DEBUG | packet_handler._phtx_ether: TX000F - Contains valid destination MAC address +21-05-30 20:53:28 | DEBUG | packet_handler._phtx_ether: TX000F - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0806 (ARP) +21-05-30 20:53:28 | DEBUG | tx_ring.enqueue: TX000F, queue len: 1 +21-05-30 20:53:28 | DEBUG | packet_handler._send_arp_probe: Sent out ARP probe for 192.168.9.53 +21-05-30 20:53:28 | DEBUG | tx_ring.__thread_transmit: [TX] TX000F - sent frame, 42 bytes +21-05-30 20:53:28 | DEBUG | packet_handler._create_stack_ip4_addressing: Sent out ARP Probe for 192.168.9.53 +21-05-30 20:53:28 | DEBUG | rx_ring.__thread_receive: [RX] RX0015> - received frame, 54 bytes +21-05-30 20:53:28 | DEBUG | packet_handler._phrx_ether: RX0015 - ETHER 5e:b6:83:7c:dc:63 > 01:00:5e:00:00:16, 0x0800 (IPv4) +21-05-30 20:53:28 | DEBUG | packet_handler._phrx_ether: RX0015 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:29 | DEBUG | rx_ring.__thread_receive: [RX] RX0016> - received frame, 86 bytes +21-05-30 20:53:29 | DEBUG | packet_handler._phrx_ether: RX0016 - ETHER 8e:ac:c2:c8:d6:ea > 02:00:00:77:77:77, 0x86dd (IPv6) +21-05-30 20:53:29 | DEBUG | packet_handler._phrx_ip6: RX0016 - IPv6 fe80::8cac:c2ff:fec8:d6ea > fe80::7, next 58 (ICMPv6), flow 0, dlen 32, hop 255 +21-05-30 20:53:29 | INFO | packet_handler._phrx_icmp6: RX0016 - ICMPv6 type 135, code 0, target fe80::7, slla 8e:ac:c2:c8:d6:ea +21-05-30 20:53:29 | DEBUG | packet_handler._phrx_icmp6: Received ICMPv6 Neighbor Solicitation packet from fe80::8cac:c2ff:fec8:d6ea, sending reply +21-05-30 20:53:29 | INFO | packet_handler._phtx_icmp6: TX0010 RX0016 - ICMPv6 type 136, code 0, target fe80::7, flags -S-, tlla 02:00:00:77:77:77 +21-05-30 20:53:29 | DEBUG | packet_handler._phtx_ip6: TX0010 RX0016 - IPv6 fe80::7 > fe80::8cac:c2ff:fec8:d6ea, next 58 (ICMPv6), flow 0, dlen 32, hop 255 +21-05-30 20:53:29 | DEBUG | packet_handler._phtx_ether: TX0010 RX0016 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:29 | DEBUG | icmp6_nd_cache.find_entry: Found fe80::8cac:c2ff:fec8:d6ea -> 8e:ac:c2:c8:d6:ea entry, age 0s, hit_count 1 +21-05-30 20:53:29 | DEBUG | packet_handler._phtx_ether: TX0010 RX0016 - Resolved destination IPv6 fe80::8cac:c2ff:fec8:d6ea to MAC 8e:ac:c2:c8:d6:ea +21-05-30 20:53:29 | DEBUG | packet_handler._phtx_ether: TX0010 RX0016 - ETHER 02:00:00:77:77:77 > 8e:ac:c2:c8:d6:ea, 0x86dd (IPv6) +21-05-30 20:53:29 | DEBUG | tx_ring.enqueue: TX0010 RX0016, queue len: 1 +21-05-30 20:53:29 | DEBUG | tx_ring.__thread_transmit: [TX] TX0010 RX0016 7.497ms - sent frame, 86 bytes +21-05-30 20:53:29 | INFO | packet_handler._phtx_arp: TX0011 - ARP request 0.0.0.0 / 02:00:00:77:77:77 > 192.168.9.7 / 00:00:00:00:00:00 +21-05-30 20:53:29 | DEBUG | packet_handler._phtx_ether: TX0011 - Contains valid destination MAC address +21-05-30 20:53:29 | DEBUG | packet_handler._phtx_ether: TX0011 - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0806 (ARP) +21-05-30 20:53:29 | DEBUG | tx_ring.enqueue: TX0011, queue len: 1 +21-05-30 20:53:29 | DEBUG | packet_handler._send_arp_probe: Sent out ARP probe for 192.168.9.7 +21-05-30 20:53:29 | DEBUG | tx_ring.__thread_transmit: [TX] TX0011 - sent frame, 42 bytes +21-05-30 20:53:29 | DEBUG | packet_handler._create_stack_ip4_addressing: Sent out ARP Probe for 192.168.9.7 +21-05-30 20:53:29 | INFO | packet_handler._phtx_arp: TX0012 - ARP request 0.0.0.0 / 02:00:00:77:77:77 > 192.168.9.53 / 00:00:00:00:00:00 +21-05-30 20:53:29 | DEBUG | packet_handler._phtx_ether: TX0012 - Contains valid destination MAC address +21-05-30 20:53:29 | DEBUG | packet_handler._phtx_ether: TX0012 - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0806 (ARP) +21-05-30 20:53:29 | DEBUG | tx_ring.enqueue: TX0012, queue len: 1 +21-05-30 20:53:29 | DEBUG | packet_handler._send_arp_probe: Sent out ARP probe for 192.168.9.53 +21-05-30 20:53:29 | DEBUG | tx_ring.__thread_transmit: [TX] TX0012 - sent frame, 42 bytes +21-05-30 20:53:29 | DEBUG | packet_handler._create_stack_ip4_addressing: Sent out ARP Probe for 192.168.9.53 +21-05-30 20:53:30 | DEBUG | rx_ring.__thread_receive: [RX] RX0017> - received frame, 241 bytes +21-05-30 20:53:30 | DEBUG | rx_ring.__thread_receive: [RX] RX0018> - received frame, 261 bytes +21-05-30 20:53:30 | DEBUG | packet_handler._phrx_ether: RX0017 - ETHER 5e:b6:83:7c:dc:63 > 01:00:5e:00:00:fb, 0x0800 (IPv4) +21-05-30 20:53:30 | DEBUG | packet_handler._phrx_ether: RX0017 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:30 | DEBUG | packet_handler._phrx_ether: RX0018 - ETHER 5e:b6:83:7c:dc:63 > 33:33:00:00:00:fb, 0x86dd (IPv6) +21-05-30 20:53:30 | DEBUG | packet_handler._phrx_ether: RX0018 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:31 | INFO | packet_handler._phtx_arp: TX0013 - ARP request 192.168.9.7 / 02:00:00:77:77:77 > 192.168.9.7 / 00:00:00:00:00:00 +21-05-30 20:53:31 | DEBUG | packet_handler._phtx_ether: TX0013 - Contains valid destination MAC address +21-05-30 20:53:31 | DEBUG | packet_handler._phtx_ether: TX0013 - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0806 (ARP) +21-05-30 20:53:31 | DEBUG | tx_ring.enqueue: TX0013, queue len: 1 +21-05-30 20:53:31 | DEBUG | packet_handler._send_arp_announcement: Sent out ARP Announcement for 192.168.9.7 +21-05-30 20:53:31 | DEBUG | tx_ring.__thread_transmit: [TX] TX0013 - sent frame, 42 bytes +21-05-30 20:53:31 | DEBUG | packet_handler._create_stack_ip4_addressing: Successfully claimed IPv4 address 192.168.9.53 +21-05-30 20:53:31 | INFO | packet_handler._phtx_arp: TX0014 - ARP request 192.168.9.53 / 02:00:00:77:77:77 > 192.168.9.53 / 00:00:00:00:00:00 +21-05-30 20:53:31 | DEBUG | packet_handler._phtx_ether: TX0014 - Contains valid destination MAC address +21-05-30 20:53:31 | DEBUG | packet_handler._phtx_ether: TX0014 - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0806 (ARP) +21-05-30 20:53:31 | DEBUG | tx_ring.enqueue: TX0014, queue len: 1 +21-05-30 20:53:31 | DEBUG | packet_handler._send_arp_announcement: Sent out ARP Announcement for 192.168.9.53 +21-05-30 20:53:31 | DEBUG | tx_ring.__thread_transmit: [TX] TX0014 - sent frame, 42 bytes +21-05-30 20:53:31 | DEBUG | packet_handler._create_stack_ip4_addressing: Successfully claimed IPv4 address 192.168.9.53 +21-05-30 20:53:31 | INFO | packet_handler.__init__: Stack listening on unicast MAC address: 02:00:00:77:77:77 +21-05-30 20:53:31 | INFO | packet_handler.__init__: Stack listening on multicast MAC addresses: ['33:33:00:00:00:01', '33:33:ff:00:00:07', '33:33:ff:77:77:77'] +21-05-30 20:53:31 | INFO | packet_handler.__init__: Stack listening on broadcast MAC address: ff:ff:ff:ff:ff:ff +21-05-30 20:53:31 | INFO | packet_handler.__init__: Stack listening on unicast IPv6 addresses: ['fe80::7', 'fe80::ff:fe77:7777', '2603:9000:e307:9f09:0:ff:fe77:7777'] +21-05-30 20:53:31 | INFO | packet_handler.__init__: Stack listening on multicast IPv6 addresses: ['ff02::1:ff77:7777', 'ff02::1', 'ff02::1:ff00:7'] +21-05-30 20:53:31 | INFO | packet_handler.__init__: Stack listening on unicast IPv4 addresses: ['192.168.9.7', '192.168.9.53'] +21-05-30 20:53:31 | INFO | packet_handler.__init__: Stack listening on multicast IPv4 addresses: [] +21-05-30 20:53:31 | INFO | packet_handler.__init__: Stack listening on broadcast IPv4 addresses: ['192.168.9.255', '192.168.9.255', '255.255.255.255'] +21-05-30 20:53:31 | DEBUG | socket.__init__: Opened UDP socket UDP/None/None/*/* +21-05-30 20:53:31 | DEBUG | socket.__init__: Created TCP socket TCP/None/None/None/None +21-05-30 20:53:31 | DEBUG | socket.bind: UDP/*/7/*/* - Socket bound to local address +21-05-30 20:53:31 | DEBUG | socket.bind: TCP/*/7/None/None - Socket bound to local address +21-05-30 20:53:31 | DEBUG | socket.listen: TCP/*/7/*/* - Socket starting to listen for inbound connections +21-05-30 20:53:31 | DEBUG | tcp_session.listen: TCP/*/7/*/* - State CLOSED - got LISTEN syscall +21-05-30 20:53:31 | INFO | tcp_session._tcp_fsm_closed: TCP/*/7/*/* - State changed: CLOSED -> LISTEN +21-05-30 20:53:31 | DEBUG | tcp_session._change_state: TCP/*/7/*/* - Registered TCP session +21-05-30 20:53:31 | DEBUG | socket.accept: TCP/*/7/*/* - Waiting for established inbound connection +21-05-30 20:53:37 | DEBUG | rx_ring.__thread_receive: [RX] RX0019> - received frame, 78 bytes +21-05-30 20:53:37 | DEBUG | packet_handler._phrx_ether: RX0019 - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:37 | DEBUG | packet_handler._phrx_ip4: RX0019 - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 64, ttl 64 +21-05-30 20:53:37 | INFO | packet_handler._phrx_tcp: RX0019 - TCP 54333 > 7, S, seq 4063510799, ack 0, win 65535, dlen 0, mss 1460, nop, wscale 6, nop, nop, ts 4127633437/0, sack_perm, eol +21-05-30 20:53:37 | DEBUG | packet_handler._phrx_tcp: RX0019 - TCP packet matches listening session TCP/*/7/*/* +21-05-30 20:53:37 | DEBUG | tcp_session.listen: TCP/*/7/*/* - State CLOSED - got LISTEN syscall +21-05-30 20:53:37 | INFO | tcp_session._tcp_fsm_closed: TCP/*/7/*/* - State changed: CLOSED -> LISTEN +21-05-30 20:53:37 | DEBUG | tcp_session._change_state: TCP/*/7/*/* - Registered TCP session +21-05-30 20:53:37 | DEBUG | tcp_session._tcp_fsm_listen: TCP/192.168.9.7/7/192.168.9.58/54333 - Initialized remote window scale at 64 +21-05-30 20:53:37 | INFO | tcp_session._tcp_fsm_listen: TCP/192.168.9.7/7/192.168.9.58/54333 - State changed: LISTEN -> SYN_RCVD +21-05-30 20:53:37 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Transmitting initial SYN + ACK packet: seq 757684438 +21-05-30 20:53:37 | INFO | packet_handler._phtx_tcp: TX0015 - TCP 7 > 54333, AS, seq 757684438, ack 4063510800, win 65535, dlen 0, mss 1460, nop, wscale 0 +21-05-30 20:53:37 | DEBUG | packet_handler._phtx_ip4: TX0015 - IPv4 192.168.9.7 > 192.168.9.58, proto 6 (TCP), id 0, offset 0, plen 48, ttl 64 +21-05-30 20:53:37 | DEBUG | packet_handler._phtx_ether: TX0015 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:37 | DEBUG | arp_cache.find_entry: Unable to find entry for 192.168.9.58, sending ARP request +21-05-30 20:53:37 | INFO | packet_handler._phtx_arp: TX0016 - ARP request 192.168.9.7 / 02:00:00:77:77:77 > 192.168.9.58 / 00:00:00:00:00:00 +21-05-30 20:53:37 | DEBUG | packet_handler._phtx_ether: TX0016 - Contains valid destination MAC address +21-05-30 20:53:37 | DEBUG | packet_handler._phtx_ether: TX0016 - ETHER 02:00:00:77:77:77 > ff:ff:ff:ff:ff:ff, 0x0806 (ARP) +21-05-30 20:53:37 | DEBUG | tx_ring.enqueue: TX0016, queue len: 1 +21-05-30 20:53:37 | DEBUG | packet_handler._phtx_ether: TX0015 - No valid destination MAC could be obtained, dropping packet... +21-05-30 20:53:37 | DEBUG | tcp_session._transmit_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent packet: SA, seq 757684438, ack 4063510800, dlen 0 +21-05-30 20:53:37 | DEBUG | tx_ring.__thread_transmit: [TX] TX0016 - sent frame, 42 bytes +21-05-30 20:53:37 | DEBUG | rx_ring.__thread_receive: [RX] RX001A> - received frame, 60 bytes +21-05-30 20:53:37 | DEBUG | packet_handler._phrx_ether: RX001A - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0806 (ARP) +21-05-30 20:53:37 | INFO | packet_handler._phrx_arp: RX001A - ARP reply 192.168.9.58 / 14:7d:da:db:35:90 > 192.168.9.7 / 02:00:00:77:77:77 +21-05-30 20:53:37 | DEBUG | packet_handler._phrx_arp: Adding/refreshing ARP cache entry from direct reply - 192.168.9.58 -> 14:7d:da:db:35:90 +21-05-30 20:53:38 | DEBUG | rx_ring.__thread_receive: [RX] RX001B> - received frame, 78 bytes +21-05-30 20:53:38 | DEBUG | packet_handler._phrx_ether: RX001B - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:38 | DEBUG | packet_handler._phrx_ip4: RX001B - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 64, ttl 64 +21-05-30 20:53:38 | INFO | packet_handler._phrx_tcp: RX001B - TCP 54333 > 7, S, seq 4063510799, ack 0, win 65535, dlen 0, mss 1460, nop, wscale 6, nop, nop, ts 4127634437/0, sack_perm, eol +21-05-30 20:53:38 | DEBUG | packet_handler._phrx_tcp: RX001B - TCP packet is part of active session TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:39 | DEBUG | tcp_session._retransmit_packet_timeout: TCP/192.168.9.7/7/192.168.9.58/54333 - Got retansmit timeout, sending segment 757684438, resetting snd_ewn to 1460 +21-05-30 20:53:39 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Transmitting initial SYN + ACK packet: seq 757684438 +21-05-30 20:53:39 | INFO | packet_handler._phtx_tcp: TX0017 - TCP 7 > 54333, AS, seq 757684438, ack 4063510800, win 65535, dlen 0, mss 1460, nop, wscale 0 +21-05-30 20:53:39 | DEBUG | packet_handler._phtx_ip4: TX0017 - IPv4 192.168.9.7 > 192.168.9.58, proto 6 (TCP), id 0, offset 0, plen 48, ttl 64 +21-05-30 20:53:39 | DEBUG | packet_handler._phtx_ether: TX0017 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:39 | DEBUG | arp_cache.find_entry: Found 192.168.9.58 -> 14:7d:da:db:35:90 entry, age 1s, hit_count 1 +21-05-30 20:53:39 | DEBUG | packet_handler._phtx_ether: TX0017 - Resolved destination IPv4 192.168.9.58 to MAC 14:7d:da:db:35:90 +21-05-30 20:53:39 | DEBUG | packet_handler._phtx_ether: TX0017 - ETHER 02:00:00:77:77:77 > 14:7d:da:db:35:90, 0x0800 (IPv4) +21-05-30 20:53:39 | DEBUG | tx_ring.enqueue: TX0017, queue len: 1 +21-05-30 20:53:39 | DEBUG | tcp_session._transmit_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent packet: SA, seq 757684438, ack 4063510800, dlen 0 +21-05-30 20:53:39 | DEBUG | tx_ring.__thread_transmit: [TX] TX0017 - sent frame, 62 bytes +21-05-30 20:53:39 | DEBUG | rx_ring.__thread_receive: [RX] RX001C> - received frame, 54 bytes +21-05-30 20:53:39 | DEBUG | packet_handler._phrx_ether: RX001C - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:39 | DEBUG | packet_handler._phrx_ip4: RX001C - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 40, ttl 64 +21-05-30 20:53:39 | INFO | packet_handler._phrx_tcp: RX001C - TCP 54333 > 7, A, seq 4063510800, ack 757684439, win 4096, dlen 0 +21-05-30 20:53:39 | DEBUG | packet_handler._phrx_tcp: RX001C - TCP packet is part of active session TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:39 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged TX buffer up to SEQ 757684439 +21-05-30 20:53:39 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated sending window size 65535 -> 262144 +21-05-30 20:53:39 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated effective sending window to 2920 +21-05-30 20:53:39 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged expired TX packet retransmit timeout for 757684438 +21-05-30 20:53:39 | INFO | tcp_session._tcp_fsm_syn_rcvd: TCP/192.168.9.7/7/192.168.9.58/54333 - State changed: SYN_RCVD -> ESTABLISHED +21-05-30 20:53:39 | DEBUG | socket.__init__: Created TCP socket TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:39 | DEBUG | socket.send: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent data segment, len 33 +21-05-30 20:53:39 | DEBUG | socket.accept: TCP/*/7/*/* - Waiting for established inbound connection +21-05-30 20:53:39 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Sliding window [757684439|757684439|757687359] +21-05-30 20:53:39 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - 2920 left in window, 33 left in buffer, 33 to be sent +21-05-30 20:53:39 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Transmitting data segment: seq 757684439 len 33 +21-05-30 20:53:39 | INFO | packet_handler._phtx_tcp: TX0018 - TCP 7 > 54333, A, seq 757684439, ack 4063510800, win 65535, dlen 33 +21-05-30 20:53:39 | DEBUG | packet_handler._phtx_ip4: TX0018 - IPv4 192.168.9.7 > 192.168.9.58, proto 6 (TCP), id 0, offset 0, plen 73, ttl 64 +21-05-30 20:53:39 | DEBUG | packet_handler._phtx_ether: TX0018 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:39 | DEBUG | arp_cache.find_entry: Found 192.168.9.58 -> 14:7d:da:db:35:90 entry, age 1s, hit_count 2 +21-05-30 20:53:39 | DEBUG | packet_handler._phtx_ether: TX0018 - Resolved destination IPv4 192.168.9.58 to MAC 14:7d:da:db:35:90 +21-05-30 20:53:39 | DEBUG | packet_handler._phtx_ether: TX0018 - ETHER 02:00:00:77:77:77 > 14:7d:da:db:35:90, 0x0800 (IPv4) +21-05-30 20:53:39 | DEBUG | tx_ring.enqueue: TX0018, queue len: 1 +21-05-30 20:53:39 | DEBUG | tcp_session._transmit_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent packet: A, seq 757684439, ack 4063510800, dlen 33 +21-05-30 20:53:39 | DEBUG | tx_ring.__thread_transmit: [TX] TX0018 - sent frame, 87 bytes +21-05-30 20:53:39 | DEBUG | rx_ring.__thread_receive: [RX] RX001D> - received frame, 54 bytes +21-05-30 20:53:39 | DEBUG | packet_handler._phrx_ether: RX001D - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:39 | DEBUG | packet_handler._phrx_ip4: RX001D - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 40, ttl 64 +21-05-30 20:53:39 | INFO | packet_handler._phrx_tcp: RX001D - TCP 54333 > 7, A, seq 4063510800, ack 757684472, win 4095, dlen 0 +21-05-30 20:53:39 | DEBUG | packet_handler._phrx_tcp: RX001D - TCP packet is part of active session TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:39 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged TX buffer up to SEQ 757684472 +21-05-30 20:53:39 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated sending window size 262144 -> 262080 +21-05-30 20:53:39 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated effective sending window to 5840 +21-05-30 20:53:39 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged expired TX packet retransmit timeout for 757684439 +21-05-30 20:53:40 | DEBUG | rx_ring.__thread_receive: [RX] RX001E> - received frame, 101 bytes +21-05-30 20:53:40 | DEBUG | rx_ring.__thread_receive: [RX] RX001F> - received frame, 121 bytes +21-05-30 20:53:40 | DEBUG | packet_handler._phrx_ether: RX001E - ETHER 54:26:96:e1:b4:05 > 01:00:5e:00:00:fb, 0x0800 (IPv4) +21-05-30 20:53:40 | DEBUG | packet_handler._phrx_ether: RX001E - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:40 | DEBUG | packet_handler._phrx_ether: RX001F - ETHER 54:26:96:e1:b4:05 > 33:33:00:00:00:fb, 0x86dd (IPv6) +21-05-30 20:53:40 | DEBUG | packet_handler._phrx_ether: RX001F - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:40 | DEBUG | rx_ring.__thread_receive: [RX] RX0020> - received frame, 236 bytes +21-05-30 20:53:40 | DEBUG | rx_ring.__thread_receive: [RX] RX0021> - received frame, 216 bytes +21-05-30 20:53:40 | DEBUG | packet_handler._phrx_ether: RX0020 - ETHER 9c:30:5b:45:bf:17 > 33:33:00:00:00:fb, 0x86dd (IPv6) +21-05-30 20:53:40 | DEBUG | packet_handler._phrx_ether: RX0020 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:40 | DEBUG | packet_handler._phrx_ether: RX0021 - ETHER 9c:30:5b:45:bf:17 > 01:00:5e:00:00:fb, 0x0800 (IPv4) +21-05-30 20:53:40 | DEBUG | packet_handler._phrx_ether: RX0021 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:41 | DEBUG | rx_ring.__thread_receive: [RX] RX0022> - received frame, 60 bytes +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_ether: RX0022 - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_ip4: RX0022 - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 46, ttl 64 +21-05-30 20:53:41 | INFO | packet_handler._phrx_tcp: RX0022 - TCP 54333 > 7, AP, seq 4063510800, ack 757684472, win 4096, dlen 6 +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_tcp: RX0022 - TCP packet is part of active session TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Enqueued 6 bytes starting at 4063510800 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged TX buffer up to SEQ 757684472 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated sending window size 262080 -> 262144 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated effective sending window to 11680 +21-05-30 20:53:41 | DEBUG | socket.receive: TCP/192.168.9.7/7/192.168.9.58/54333 - Received 6 bytes of data +21-05-30 20:53:41 | DEBUG | socket.send: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent data segment, len 1561 +21-05-30 20:53:41 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Sliding window [757684472|757684472|757696152] +21-05-30 20:53:41 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - 11680 left in window, 1561 left in buffer, 1460 to be sent +21-05-30 20:53:41 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Transmitting data segment: seq 757684472 len 1460 +21-05-30 20:53:41 | INFO | packet_handler._phtx_tcp: TX0019 - TCP 7 > 54333, A, seq 757684472, ack 4063510806, win 65535, dlen 1460 +21-05-30 20:53:41 | DEBUG | packet_handler._phtx_ip4: TX0019 - IPv4 192.168.9.7 > 192.168.9.58, proto 6 (TCP), id 0, offset 0, plen 1500, ttl 64 +21-05-30 20:53:41 | DEBUG | packet_handler._phtx_ether: TX0019 - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:41 | DEBUG | arp_cache.find_entry: Found 192.168.9.58 -> 14:7d:da:db:35:90 entry, age 3s, hit_count 3 +21-05-30 20:53:41 | DEBUG | packet_handler._phtx_ether: TX0019 - Resolved destination IPv4 192.168.9.58 to MAC 14:7d:da:db:35:90 +21-05-30 20:53:41 | DEBUG | packet_handler._phtx_ether: TX0019 - ETHER 02:00:00:77:77:77 > 14:7d:da:db:35:90, 0x0800 (IPv4) +21-05-30 20:53:41 | DEBUG | tx_ring.enqueue: TX0019, queue len: 1 +21-05-30 20:53:41 | DEBUG | tcp_session._transmit_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent packet: A, seq 757684472, ack 4063510806, dlen 1460 +21-05-30 20:53:41 | DEBUG | tx_ring.__thread_transmit: [TX] TX0019 - sent frame, 1514 bytes +21-05-30 20:53:41 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Sliding window [757684472|757685932|757696152] +21-05-30 20:53:41 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - 10220 left in window, 101 left in buffer, 101 to be sent +21-05-30 20:53:41 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Transmitting data segment: seq 757685932 len 101 +21-05-30 20:53:41 | INFO | packet_handler._phtx_tcp: TX001A - TCP 7 > 54333, A, seq 757685932, ack 4063510806, win 65535, dlen 101 +21-05-30 20:53:41 | DEBUG | rx_ring.__thread_receive: [RX] RX0023> - received frame, 54 bytes +21-05-30 20:53:41 | DEBUG | packet_handler._phtx_ip4: TX001A - IPv4 192.168.9.7 > 192.168.9.58, proto 6 (TCP), id 0, offset 0, plen 141, ttl 64 +21-05-30 20:53:41 | DEBUG | packet_handler._phtx_ether: TX001A - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_ether: RX0023 - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:41 | DEBUG | arp_cache.find_entry: Found 192.168.9.58 -> 14:7d:da:db:35:90 entry, age 3s, hit_count 4 +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_ip4: RX0023 - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 40, ttl 64 +21-05-30 20:53:41 | DEBUG | packet_handler._phtx_ether: TX001A - Resolved destination IPv4 192.168.9.58 to MAC 14:7d:da:db:35:90 +21-05-30 20:53:41 | INFO | packet_handler._phrx_tcp: RX0023 - TCP 54333 > 7, A, seq 4063510806, ack 757685932, win 4073, dlen 0 +21-05-30 20:53:41 | DEBUG | packet_handler._phtx_ether: TX001A - ETHER 02:00:00:77:77:77 > 14:7d:da:db:35:90, 0x0800 (IPv4) +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_tcp: RX0023 - TCP packet is part of active session TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:41 | DEBUG | tx_ring.enqueue: TX001A, queue len: 1 +21-05-30 20:53:41 | DEBUG | tcp_session._transmit_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent packet: A, seq 757685932, ack 4063510806, dlen 101 +21-05-30 20:53:41 | DEBUG | tx_ring.__thread_transmit: [TX] TX001A - sent frame, 155 bytes +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged TX buffer up to SEQ 757685932 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated sending window size 262144 -> 260672 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated effective sending window to 23360 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged expired TX packet retransmit timeout for 757684472 +21-05-30 20:53:41 | DEBUG | rx_ring.__thread_receive: [RX] RX0024> - received frame, 54 bytes +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_ether: RX0024 - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_ip4: RX0024 - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 40, ttl 64 +21-05-30 20:53:41 | INFO | packet_handler._phrx_tcp: RX0024 - TCP 54333 > 7, A, seq 4063510806, ack 757686033, win 4094, dlen 0 +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_tcp: RX0024 - TCP packet is part of active session TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged TX buffer up to SEQ 757686033 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated sending window size 260672 -> 262016 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated effective sending window to 46720 +21-05-30 20:53:41 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged expired TX packet retransmit timeout for 757685932 +21-05-30 20:53:41 | DEBUG | rx_ring.__thread_receive: [RX] RX0025> - received frame, 62 bytes +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_ether: RX0025 - ETHER f8:e7:1e:2a:dc:b0 > 33:33:00:00:00:02, 0x86dd (IPv6) +21-05-30 20:53:41 | DEBUG | packet_handler._phrx_ether: RX0025 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:42 | DEBUG | rx_ring.__thread_receive: [RX] RX0026> - received frame, 180 bytes +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ether: RX0026 - ETHER 52:54:00:5a:cb:e2 > 33:33:00:01:00:02, 0x86dd (IPv6) +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ether: RX0026 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:42 | DEBUG | rx_ring.__thread_receive: [RX] RX0027> - received frame, 110 bytes +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ether: RX0027 - ETHER 52:54:00:5a:cb:e2 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ether: RX0027 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:42 | DEBUG | rx_ring.__thread_receive: [RX] RX0028> - received frame, 86 bytes +21-05-30 20:53:42 | DEBUG | rx_ring.__thread_receive: [RX] RX0029> - received frame, 86 bytes +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ether: RX0028 - ETHER 52:54:00:5a:cb:e2 > 33:33:ff:00:11:18, 0x86dd (IPv6) +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ether: RX0028 - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ether: RX0029 - ETHER 52:54:00:67:62:f0 > 33:33:00:00:00:01, 0x86dd (IPv6) +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ip6: RX0029 - IPv6 2603:9000:e307:9f09::1118 > ff02::1, next 58 (ICMPv6), flow 0, dlen 32, hop 255 +21-05-30 20:53:42 | INFO | packet_handler._phrx_icmp6: RX0029 - ICMPv6 type 136, code 0, target 2603:9000:e307:9f09::1118, flags --O, tlla 52:54:00:67:62:f0 +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_icmp6: Received ICMPv6 Neighbor Advertisement packet for 2603:9000:e307:9f09::1118 from 2603:9000:e307:9f09::1118 +21-05-30 20:53:42 | DEBUG | rx_ring.__thread_receive: [RX] RX002A> - received frame, 110 bytes +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ether: RX002A - ETHER 52:54:00:5a:cb:e2 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:42 | DEBUG | packet_handler._phrx_ether: RX002A - Ethernet packet not destined for this stack, dropping... +21-05-30 20:53:43 | DEBUG | rx_ring.__thread_receive: [RX] RX002B> - received frame, 54 bytes +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_ether: RX002B - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_ip4: RX002B - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 40, ttl 64 +21-05-30 20:53:43 | INFO | packet_handler._phrx_tcp: RX002B - TCP 54333 > 7, AF, seq 4063510806, ack 757686033, win 4096, dlen 0 +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_tcp: RX002B - TCP packet is part of active session TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:43 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged TX buffer up to SEQ 757686033 +21-05-30 20:53:43 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated sending window size 262016 -> 262144 +21-05-30 20:53:43 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated effective sending window to 93440 +21-05-30 20:53:43 | INFO | tcp_session._tcp_fsm_established: TCP/192.168.9.7/7/192.168.9.58/54333 - State changed: ESTABLISHED -> CLOSE_WAIT +21-05-30 20:53:43 | DEBUG | socket.receive: TCP/192.168.9.7/7/192.168.9.58/54333 - Received close event from TCP session +21-05-30 20:53:43 | DEBUG | socket.send: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent data segment, len -1 +21-05-30 20:53:43 | DEBUG | tcp_session.close: TCP/192.168.9.7/7/192.168.9.58/54333 - State CLOSE_WAIT - got CLOSE syscall, 37 bytes in TX buffer +21-05-30 20:53:43 | DEBUG | socket.close: TCP/192.168.9.7/7/192.168.9.58/54333 - Closed socket +21-05-30 20:53:43 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Sliding window [757686033|757686033|757779473] +21-05-30 20:53:43 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - 93440 left in window, 37 left in buffer, 37 to be sent +21-05-30 20:53:43 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Transmitting data segment: seq 757686033 len 37 +21-05-30 20:53:43 | INFO | packet_handler._phtx_tcp: TX001B - TCP 7 > 54333, A, seq 757686033, ack 4063510807, win 65535, dlen 37 +21-05-30 20:53:43 | DEBUG | packet_handler._phtx_ip4: TX001B - IPv4 192.168.9.7 > 192.168.9.58, proto 6 (TCP), id 0, offset 0, plen 77, ttl 64 +21-05-30 20:53:43 | DEBUG | packet_handler._phtx_ether: TX001B - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:43 | DEBUG | arp_cache.find_entry: Found 192.168.9.58 -> 14:7d:da:db:35:90 entry, age 5s, hit_count 5 +21-05-30 20:53:43 | DEBUG | packet_handler._phtx_ether: TX001B - Resolved destination IPv4 192.168.9.58 to MAC 14:7d:da:db:35:90 +21-05-30 20:53:43 | DEBUG | packet_handler._phtx_ether: TX001B - ETHER 02:00:00:77:77:77 > 14:7d:da:db:35:90, 0x0800 (IPv4) +21-05-30 20:53:43 | DEBUG | tx_ring.enqueue: TX001B, queue len: 1 +21-05-30 20:53:43 | DEBUG | tcp_session._transmit_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent packet: A, seq 757686033, ack 4063510807, dlen 37 +21-05-30 20:53:43 | DEBUG | tx_ring.__thread_transmit: [TX] TX001B - sent frame, 91 bytes +21-05-30 20:53:43 | DEBUG | rx_ring.__thread_receive: [RX] RX002C> - received frame, 54 bytes +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_ether: RX002C - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_ip4: RX002C - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 40, ttl 64 +21-05-30 20:53:43 | INFO | packet_handler._phrx_tcp: RX002C - TCP 54333 > 7, A, seq 4063510807, ack 757686070, win 4095, dlen 0 +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_tcp: RX002C - TCP packet is part of active session TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:43 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged TX buffer up to SEQ 757686070 +21-05-30 20:53:43 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated sending window size 262144 -> 262080 +21-05-30 20:53:43 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Updated effective sending window to 186880 +21-05-30 20:53:43 | DEBUG | tcp_session._process_ack_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Purged expired TX packet retransmit timeout for 757686033 +21-05-30 20:53:43 | INFO | tcp_session._tcp_fsm_close_wait: TCP/192.168.9.7/7/192.168.9.58/54333 - State changed: CLOSE_WAIT -> LAST_ACK +21-05-30 20:53:43 | DEBUG | tcp_session._transmit_data: TCP/192.168.9.7/7/192.168.9.58/54333 - Transmitting final FIN packet: seq 757686070 +21-05-30 20:53:43 | INFO | packet_handler._phtx_tcp: TX001C - TCP 7 > 54333, AF, seq 757686070, ack 4063510807, win 65535, dlen 0 +21-05-30 20:53:43 | DEBUG | packet_handler._phtx_ip4: TX001C - IPv4 192.168.9.7 > 192.168.9.58, proto 6 (TCP), id 0, offset 0, plen 40, ttl 64 +21-05-30 20:53:43 | DEBUG | packet_handler._phtx_ether: TX001C - Set source to stack MAC 02:00:00:77:77:77 +21-05-30 20:53:43 | DEBUG | arp_cache.find_entry: Found 192.168.9.58 -> 14:7d:da:db:35:90 entry, age 5s, hit_count 6 +21-05-30 20:53:43 | DEBUG | packet_handler._phtx_ether: TX001C - Resolved destination IPv4 192.168.9.58 to MAC 14:7d:da:db:35:90 +21-05-30 20:53:43 | DEBUG | packet_handler._phtx_ether: TX001C - ETHER 02:00:00:77:77:77 > 14:7d:da:db:35:90, 0x0800 (IPv4) +21-05-30 20:53:43 | DEBUG | tx_ring.enqueue: TX001C, queue len: 1 +21-05-30 20:53:43 | DEBUG | tcp_session._transmit_packet: TCP/192.168.9.7/7/192.168.9.58/54333 - Sent packet: FA, seq 757686070, ack 4063510807, dlen 0 +21-05-30 20:53:43 | DEBUG | tx_ring.__thread_transmit: [TX] TX001C - sent frame, 54 bytes +21-05-30 20:53:43 | DEBUG | rx_ring.__thread_receive: [RX] RX002D> - received frame, 54 bytes +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_ether: RX002D - ETHER 14:7d:da:db:35:90 > 02:00:00:77:77:77, 0x0800 (IPv4) +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_ip4: RX002D - IPv4 192.168.9.58 > 192.168.9.7, proto 6 (TCP), id 0, DF, offset 0, plen 40, ttl 64 +21-05-30 20:53:43 | INFO | packet_handler._phrx_tcp: RX002D - TCP 54333 > 7, A, seq 4063510807, ack 757686071, win 4096, dlen 0 +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_tcp: RX002D - TCP packet is part of active session TCP/192.168.9.7/7/192.168.9.58/54333 +21-05-30 20:53:43 | INFO | tcp_session._tcp_fsm_last_ack: TCP/192.168.9.7/7/192.168.9.58/54333 - State changed: LAST_ACK -> CLOSED +21-05-30 20:53:43 | DEBUG | tcp_session._change_state: TCP/192.168.9.7/7/192.168.9.58/54333 - Unregistered TCP session +21-05-30 20:53:43 | DEBUG | rx_ring.__thread_receive: [RX] RX002E> - received frame, 90 bytes +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_ether: RX002E - ETHER 52:54:00:5a:cb:e2 > 33:33:00:00:00:16, 0x86dd (IPv6) +21-05-30 20:53:43 | DEBUG | packet_handler._phrx_ether: RX002E - Ethernet packet not destined for this stack, dropping... diff --git a/misc/__init__.py b/misc/__init__.py new file mode 100755 index 00000000..4626c9ed --- /dev/null +++ b/misc/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# misc/__init__.py +# diff --git a/arp_cache.py b/misc/arp_cache.py similarity index 68% rename from arp_cache.py rename to misc/arp_cache.py index 59364a1d..d5bf5133 100755 --- a/arp_cache.py +++ b/misc/arp_cache.py @@ -23,34 +23,22 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# arp_cache.py - module contains class supporting ARP cache +# misc/arp_cache.py - module contains class supporting ARP cache # import time +from typing import Optional, cast import loguru -import fpa_arp -import stack -from ipv4_address import IPv4Address - -ARP_ENTRY_MAX_AGE = 3600 -ARP_ENTRY_REFRESH_TIME = 300 +import arp.ps +import config +import misc.stack as stack +from misc.ipv4_address import IPv4Address +from misc.timer import Timer class ArpCache: @@ -59,29 +47,28 @@ class ArpCache: class CacheEntry: """Container class for cache entries""" - def __init__(self, mac_address, permanent=False): + def __init__(self, mac_address: str, permanent: bool = False) -> None: self.mac_address = mac_address self.permanent = permanent self.creation_time = time.time() self.hit_count = 0 - def __init__(self, packet_handler): + def __init__(self) -> None: """Class constructor""" - self.packet_handler = packet_handler - - self.arp_cache = {} + self.arp_cache: dict[str, ArpCache.CacheEntry] = {} if __debug__: self._logger = loguru.logger.bind(object_name="arp_cache.") # Setup timer to execute ARP Cache maintainer every second + stack.timer = cast(Timer, stack.timer) stack.timer.register_method(method=self._maintain_cache, delay=1000) if __debug__: self._logger.debug("Started ARP cache") - def _maintain_cache(self): + def _maintain_cache(self) -> None: """Method responsible for maintaining ARP cache entries""" for ip4_address in list(self.arp_cache): @@ -91,26 +78,28 @@ def _maintain_cache(self): continue # If entry age is over maximum age then discard the entry - if time.time() - self.arp_cache[ip4_address].creation_time > ARP_ENTRY_MAX_AGE: + if time.time() - self.arp_cache[ip4_address].creation_time > config.arp_cache_entry_max_age: mac_address = self.arp_cache.pop(ip4_address).mac_address if __debug__: self._logger.debug(f"Discarded expired ARP cache entry - {ip4_address} -> {mac_address}") # If entry age is close to maximum age but the entry has been used since last refresh then send out request in attempt to refresh it - elif (time.time() - self.arp_cache[ip4_address].creation_time > ARP_ENTRY_MAX_AGE - ARP_ENTRY_REFRESH_TIME) and self.arp_cache[ - ip4_address - ].hit_count: + elif ( + time.time() - self.arp_cache[ip4_address].creation_time > config.arp_cache_entry_max_age - config.arp_cache_entry_refresh_time + ) and self.arp_cache[ip4_address].hit_count: self.arp_cache[ip4_address].hit_count = 0 self._send_arp_request(ip4_address) if __debug__: self._logger.debug(f"Trying to refresh expiring ARP cache entry for {ip4_address} -> {self.arp_cache[ip4_address].mac_address}") - def add_entry(self, ip4_address, mac_address): + # typing: Need to refactor type of ip4_address (str -> IPv4Address) + def add_entry(self, ip4_address: str, mac_address: str) -> None: """Add / refresh entry in cache""" self.arp_cache[ip4_address] = self.CacheEntry(mac_address) - def find_entry(self, ip4_address): + # typing: Need to refactor type of ip4_address (str -> IPv4Address) + def find_entry(self, ip4_address: str) -> Optional[str]: """Find entry in cache and return MAC address""" if arp_entry := self.arp_cache.get(ip4_address, None): @@ -126,15 +115,17 @@ def find_entry(self, ip4_address): self._send_arp_request(ip4_address) return None + # typing: Need to refactor type of arp_tpa (str -> IPv4Address) + # typing: Need to fix ssue with stac.packet_handler being initially None def _send_arp_request(self, arp_tpa): """Enqueue ARP request packet with TX ring""" - self.packet_handler._phtx_arp( - ether_src=self.packet_handler.mac_unicast, + stack.packet_handler._phtx_arp( + ether_src=stack.packet_handler.mac_unicast, ether_dst="ff:ff:ff:ff:ff:ff", - arp_oper=fpa_arp.ARP_OP_REQUEST, - arp_sha=self.packet_handler.mac_unicast, - arp_spa=self.packet_handler.ip4_unicast[0] if self.packet_handler.ip4_unicast else IPv4Address("0.0.0.0"), + arp_oper=arp.ps.OP_REQUEST, + arp_sha=stack.packet_handler.mac_unicast, + arp_spa=stack.packet_handler.ip4_unicast[0] if stack.packet_handler.ip4_unicast else IPv4Address("0.0.0.0"), arp_tha="00:00:00:00:00:00", arp_tpa=arp_tpa, ) diff --git a/ip_helper.py b/misc/ip_helper.py similarity index 65% rename from ip_helper.py rename to misc/ip_helper.py index 74c99c66..89e4480d 100755 --- a/ip_helper.py +++ b/misc/ip_helper.py @@ -23,32 +23,21 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# ip_helper.py - module contains IPv6 helper functions +# misc/ip_helper.py - module contains helper functions # import struct from ipaddress import AddressValueError +from typing import Union -from ipv4_address import IPv4Address -from ipv6_address import IPv6Address +from misc.ipv4_address import IPv4Address +from misc.ipv6_address import IPv6Address -def inet_cksum(data, dptr, dlen, init=0): +def inet_cksum(data: bytes, dptr: int, dlen: int, init: int = 0) -> int: """Compute Internet Checksum used by IPv4/ICMPv4/ICMPv6/UDP/TCP protocols""" if dlen == 20: @@ -65,8 +54,8 @@ def inet_cksum(data, dptr, dlen, init=0): return ~(cksum + (cksum >> 16)) & 0xFFFF -def ip_pick_version(ip_address): - """Return correct IPv6Address or IPv4Address based on address string provided""" +def ip_pick_version(ip_address: str) -> Union[IPv6Address, IPv4Address]: + """Return correct IPv6Address or IPv4Address object based on address string provided""" try: return IPv6Address(ip_address) diff --git a/ipv4_address.py b/misc/ipv4_address.py similarity index 62% rename from ipv4_address.py rename to misc/ipv4_address.py index df426312..ee783ac2 100755 --- a/ipv4_address.py +++ b/misc/ipv4_address.py @@ -23,69 +23,75 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# ipv4_address.py - module contains IPv4 address manipulation classes (extensions to ipaddress standard library) +# misc/ipv4_address.py - module contains IPv4 address manipulation classes (extensions to ipaddress standard library) # import ipaddress +from typing import Optional, Union + + +class IPv4Address(ipaddress.IPv4Address): + """Extensions for ipaddress.IPv4Address class""" + + def __init__(self, ip4_address: Union[ipaddress.IPv4Address, str, bytes]) -> None: + """Class constructor""" + + super().__init__(ip4_address) + + @property + def is_limited_broadcast(self) -> bool: + """Check if IPv4 address is a limited broadcast""" + + return str(self) == "255.255.255.255" + + +class IPv4Network(ipaddress.IPv4Network): + """Extensions for ipaddress.IPv4Network class""" + + def __init__(self, ip4_network: Union[ipaddress.IPv4Network, str]) -> None: + """Class constructor""" + + super().__init__(ip4_network) class IPv4Interface(ipaddress.IPv4Interface): """Extensions for ipaddress.IPv4Address class""" + def __init__(self, ip4_interface: Union[ipaddress.IPv4Interface, str]) -> None: + """Class constructor""" + + self.gateway: Optional[IPv4Address] = None + + super().__init__(ip4_interface) + @property - def ip(self): - """Make sure class returns overloaded IPv6Address object""" + def ip(self) -> IPv4Address: + """Make sure class returns overloaded IPv4Address object""" return IPv4Address(super().ip) @property - def host_address(self): + def host_address(self) -> IPv4Address: """Return host address""" return self.ip @property - def network_address(self): + def network_address(self) -> IPv4Address: """Return network address""" return IPv4Address(self.network.network_address) @property - def broadcast_address(self): + def broadcast_address(self) -> IPv4Address: """Return broadcast address""" return IPv4Address(self.network.broadcast_address) @property - def is_limited_broadcast(self): - """Check if IPv4 address is a limited broadcast""" - - return self.ip.is_limited_broadcast - - -class IPv4Network(ipaddress.IPv4Network): - """Extensions for ipaddress.IPv4Network class""" + def is_limited_broadcast(self) -> bool: + """Create IPv6 solicited node multicast address""" - -class IPv4Address(ipaddress.IPv4Address): - """Extensions for ipaddress.IPv4Address class""" - - @property - def is_limited_broadcast(self): - """Check if IPv4 address is a limited broadcast""" - - return str(self) == "255.255.255.255" + return IPv4Address(super().ip).is_limited_broadcast diff --git a/ipv6_address.py b/misc/ipv6_address.py similarity index 67% rename from ipv6_address.py rename to misc/ipv6_address.py index 578f1604..de72f177 100755 --- a/ipv6_address.py +++ b/misc/ipv6_address.py @@ -23,83 +23,61 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# ipv6_address.py - module contains IPv6 address manipulation classes (extensions to ipaddress standard library) +# misc/ipv6_address.py - module contains IPv6 address manipulation classes (extensions to ipaddress standard library) # +from __future__ import annotations # Required for Python version lower than 3.10 + import ipaddress from re import sub -class IPv6Interface(ipaddress.IPv6Interface): +class IPv6Address(ipaddress.IPv6Address): """Extensions for ipaddress.IPv6Address class""" - @property - def ip(self): - """Make sure class returns overloaded IPv6Address object""" + def __init__(self, ip6_address: ipaddress.IPv6Address | str | int | bytes) -> None: + """Class constructor""" - return IPv6Address(super().ip) + super().__init__(ip6_address) @property - def host_address(self): - """Return host address""" - - return self.ip - - @property - def solicited_node_multicast(self): + def solicited_node_multicast(self) -> IPv6Address: """Create IPv6 solicited node multicast address""" - return self.ip.solicited_node_multicast + return IPv6Address("ff02::1:ff" + self.exploded[-7:]) @property - def is_solicited_node_multicast(self): + def is_solicited_node_multicast(self) -> bool: """Check if address is IPv6 solicited node multicast address""" - return self.ip.is_solicited_node_multicast + return str(self).startswith("ff02::1:ff") @property - def is_unicast(self): + def is_unicast(self) -> bool: """Check if address is IPv6 unicast address""" - return self.ip.is_unicast - - @property - def is_reserved(self): - """Check if address is IPv6 reserved address""" - - return self.ip.is_reserved + return not (self.is_multicast or self.is_unspecified) @property - def is_unspecified(self): - """Check if address is IPv6 unspecified address""" - - return self.ip.is_unspecified + def multicast_mac(self) -> str: + """Create IPv6 multicast MAC address""" - @property - def is_multicast(self): - """Check if address is IPv6 multicast address""" + assert self.is_multicast - return self.ip.is_multicast + return "33:33:" + ":".join(["".join(self.exploded[-9:].split(":"))[_ : _ + 2] for _ in range(0, 8, 2)]) class IPv6Network(ipaddress.IPv6Network): """Extensions for ipaddress.IPv6Network class""" - def eui64(self, mac): + def __init__(self, ip6_network: ipaddress.IPv6Network | str | int | tuple[bytes, int]) -> None: + """Class constructor""" + + super().__init__(ip6_network) + + def eui64(self, mac: str) -> IPv6Interface: """Create IPv6 EUI64 interface address""" assert self.prefixlen == 64 @@ -111,31 +89,60 @@ def eui64(self, mac): return IPv6Interface(self.network_address.exploded[0:20] + eui64 + "/" + str(self.prefixlen)) -class IPv6Address(ipaddress.IPv6Address): +class IPv6Interface(ipaddress.IPv6Interface): """Extensions for ipaddress.IPv6Address class""" + def __init__(self, ip6_interface: ipaddress.IPv6Interface | str) -> None: + """Class constructor""" + + self.gateway: IPv6Address | None = None + + super().__init__(ip6_interface) + + @property + def ip(self) -> ipaddress.IPv6Address: + """Make sure class returns overloaded IPv6Address object""" + + return IPv6Address(super().ip) + + @property + def host_address(self) -> ipaddress.IPv6Address: + """Return host address""" + + return self.ip + @property - def solicited_node_multicast(self): + def solicited_node_multicast(self) -> ipaddress.IPv6Address: """Create IPv6 solicited node multicast address""" - return IPv6Address("ff02::1:ff" + self.exploded[-7:]) + return IPv6Address(super().ip).solicited_node_multicast @property - def is_solicited_node_multicast(self): + def is_solicited_node_multicast(self) -> bool: """Check if address is IPv6 solicited node multicast address""" - return str(self).startswith("ff02::1:ff") + return IPv6Address(super().ip).is_solicited_node_multicast @property - def is_unicast(self): + def is_unicast(self) -> bool: """Check if address is IPv6 unicast address""" - return not (self.is_multicast or self.is_unspecified) + return IPv6Address(super().ip).is_unicast @property - def multicast_mac(self): - """Create IPv6 multicast MAC address""" + def is_reserved(self) -> bool: + """Check if address is IPv6 reserved address""" - assert self.is_multicast + return IPv6Address(super().ip).is_reserved - return "33:33:" + ":".join(["".join(self.exploded[-9:].split(":"))[_ : _ + 2] for _ in range(0, 8, 2)]) + @property + def is_unspecified(self) -> bool: + """Check if address is IPv6 unspecified address""" + + return IPv6Address(super().ip).is_unspecified + + @property + def is_multicast(self) -> bool: + """Check if address is IPv6 multicast address""" + + return IPv6Address(super().ip).is_multicast diff --git a/malpi.py b/misc/malpi.py similarity index 77% rename from malpi.py rename to misc/malpi.py index 8b2483f8..e1ffa13f 100755 --- a/malpi.py +++ b/misc/malpi.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# malpi.py - module contains test monkeys used by various services +# misc/malpi.py - module contains test monkeys used by various services # diff --git a/icmp6_nd_cache.py b/misc/nd_cache.py similarity index 70% rename from icmp6_nd_cache.py rename to misc/nd_cache.py index b675b6e1..9c57fe19 100755 --- a/icmp6_nd_cache.py +++ b/misc/nd_cache.py @@ -23,65 +23,52 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# icmp6_nd_cache.py - module contains class supporting ICMPv6 Neighbor Discovery cache +# misc/nd_cache.py - module contains class supporting ICMPv6 Neighbor Discovery cache # import time +from typing import Optional, cast import loguru -import fpa_icmp6 -import stack -from ipv6_address import IPv6Address - -ND_ENTRY_MAX_AGE = 3600 -ND_ENTRY_REFRESH_TIME = 300 +import config +import icmp6.fpa +import misc.stack as stack +from misc.ipv6_address import IPv6Address +from misc.timer import Timer -class ICMPv6NdCache: +class NdCache: """Support for ICMPv6 ND cache operations""" class CacheEntry: """Container class for cache entries""" - def __init__(self, mac_address, permanent=False): + def __init__(self, mac_address: str, permanent: bool = False) -> None: self.mac_address = mac_address self.permanent = permanent self.creation_time = time.time() self.hit_count = 0 - def __init__(self, packet_handler): + def __init__(self) -> None: """Class constructor""" - self.packet_handler = packet_handler - - self.nd_cache = {} + self.nd_cache: dict[str, NdCache.CacheEntry] = {} if __debug__: self._logger = loguru.logger.bind(object_name="icmp6_nd_cache.") # Setup timer to execute ND Cache maintainer every second + stack.timer = cast(Timer, stack.timer) stack.timer.register_method(method=self._maintain_cache, delay=1000) if __debug__: self._logger.debug("Started ICMPv6 Neighbor Discovery cache") - def _maintain_cache(self): + def _maintain_cache(self) -> None: """Method responsible for maintaining ND cache entries""" for ip6_address in list(self.nd_cache): @@ -91,24 +78,28 @@ def _maintain_cache(self): continue # If entry age is over maximum age then discard the entry - if time.time() - self.nd_cache[ip6_address].creation_time > ND_ENTRY_MAX_AGE: + if time.time() - self.nd_cache[ip6_address].creation_time > config.nd_cache_entry_max_age: mac_address = self.nd_cache.pop(ip6_address).mac_address if __debug__: self._logger.debug(f"Discarded expired ICMPv6 ND cache entry - {ip6_address} -> {mac_address}") # If entry age is close to maximum age but the entry has been used since last refresh then send out request in attempt to refresh it - elif (time.time() - self.nd_cache[ip6_address].creation_time > ND_ENTRY_MAX_AGE - ND_ENTRY_REFRESH_TIME) and self.nd_cache[ip6_address].hit_count: + elif ( + time.time() - self.nd_cache[ip6_address].creation_time > config.nd_cache_entry_max_age - config.nd_cache_entry_refresh_time + ) and self.nd_cache[ip6_address].hit_count: self.nd_cache[ip6_address].hit_count = 0 self._send_icmp6_neighbor_solicitation(ip6_address) if __debug__: self._logger.debug(f"Trying to refresh expiring ICMPv6 ND cache entry for {ip6_address} -> {self.nd_cache[ip6_address].mac_address}") - def add_entry(self, ip6_address, mac_address): + # typing: Need to refactor type of ip6_address (str -> IPv6Address) + def add_entry(self, ip6_address: str, mac_address: str) -> None: """Add / refresh entry in cache""" self.nd_cache[ip6_address] = self.CacheEntry(mac_address) - def find_entry(self, ip6_address): + # typing: Need to refactor type of ip6_address (str -> IPv6Address) + def find_entry(self, ip6_address: str) -> Optional[str]: """Find entry in cache and return MAC address""" if nd_entry := self.nd_cache.get(ip6_address, None): @@ -124,21 +115,23 @@ def find_entry(self, ip6_address): self._send_icmp6_neighbor_solicitation(ip6_address) return None + # typing: Need to refactor type of arp_tpa (str -> IPv6Address) + # typing: Need to fix ssue with stac.packet_handler being initially None def _send_icmp6_neighbor_solicitation(self, icmp6_ns_target_address): """Enqueue ICMPv6 Neighbor Solicitation packet with TX ring""" # Pick appropriate source address ip6_src = IPv6Address("::") - for ip6_address in self.packet_handler.ip6_address: + for ip6_address in stack.packet_handler.ip6_address: if icmp6_ns_target_address in ip6_address.network: ip6_src = ip6_address.ip # Send out ND Solicitation message - self.packet_handler._phtx_icmp6( + stack.packet_handler._phtx_icmp6( ip6_src=ip6_src, ip6_dst=icmp6_ns_target_address.solicited_node_multicast, ip6_hop=255, - icmp6_type=fpa_icmp6.ICMP6_NEIGHBOR_SOLICITATION, + icmp6_type=icmp6.ps.NEIGHBOR_SOLICITATION, icmp6_ns_target_address=icmp6_ns_target_address, - icmp6_nd_options=[fpa_icmp6.Icmp6NdOptSLLA(self.packet_handler.mac_unicast)], + icmp6_nd_options=[icmp6.fpa.NdOptSLLA(stack.packet_handler.mac_unicast)], ) diff --git a/packet.py b/misc/packet.py similarity index 63% rename from packet.py rename to misc/packet.py index d19ce092..e380d927 100755 --- a/packet.py +++ b/misc/packet.py @@ -23,38 +23,39 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # # packet.py - module contains class representing packet # -from tracker import Tracker +from typing import Optional + +from misc.tracker import Tracker class PacketRx: """Base packet class""" - def __init__(self, frame): + def __init__(self, frame: bytes) -> None: """Class constructor""" self.frame = frame self.hptr = 0 self.tracker = Tracker("RX") - self.parse_failed = None - - def __len__(self): + self.parse_failed = Optional[str] + + self.ether: object = None + self.arp: object = None + self.ip: object = None + self.ip4: object = None + self.ip6: object = None + self.ip6_ext_frag: object = None + self.icmp4: object = None + self.icmp6: object = None + self.tcp: object = None + self.udp: object = None + + def __len__(self) -> int: """Returns length of raw frame""" return len(self.frame) diff --git a/ph.py b/misc/ph.py similarity index 69% rename from ph.py rename to misc/ph.py index d855686e..ca1d5870 100755 --- a/ph.py +++ b/misc/ph.py @@ -23,70 +23,62 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# ph.py - protocol support for incoming and outgoing packets +# ph.py - packet handler for inbound and outbound packets # import random import threading import time from ipaddress import AddressValueError +from typing import Optional, cast import loguru +import arp.ps import config -import fpa_arp -import fpa_icmp6 -import ps_dhcp -import stack -from arp_cache import ArpCache -from icmp6_nd_cache import ICMPv6NdCache -from ipv4_address import IPv4Address, IPv4Interface -from ipv6_address import IPv6Address, IPv6Interface, IPv6Network -from rx_ring import RxRing -from tx_ring import TxRing -from udp_metadata import UdpMetadata -from udp_socket import UdpSocket +import icmp6.fpa +import icmp6.ps +import misc.stack as stack +from misc.arp_cache import ArpCache +from misc.ipv4_address import IPv4Address, IPv4Interface +from misc.ipv6_address import IPv6Address, IPv6Interface, IPv6Network +from misc.nd_cache import NdCache +from misc.rx_ring import RxRing +from misc.tx_ring import TxRing class PacketHandler: """Pick up and respond to incoming packets""" - from phrx_arp import _phrx_arp - from phrx_ether import _phrx_ether - from phrx_icmp4 import _phrx_icmp4 - from phrx_icmp6 import _phrx_icmp6 - from phrx_ip4 import _phrx_ip4 - from phrx_ip6 import _phrx_ip6 - from phrx_ip6_ext_frag import _phrx_ip6_ext_frag - from phrx_tcp import _phrx_tcp - from phrx_udp import _phrx_udp - from phtx_arp import _phtx_arp - from phtx_ether import _phtx_ether - from phtx_icmp4 import _phtx_icmp4 - from phtx_icmp6 import _phtx_icmp6 - from phtx_ip4 import _phtx_ip4 - from phtx_ip6 import _phtx_ip6 - from phtx_ip6_ext_frag import _phtx_ip6_ext_frag - from phtx_tcp import _phtx_tcp - from phtx_udp import _phtx_udp + from arp.phrx import _phrx_arp + from arp.phtx import _phtx_arp + from dhcp4.client import _dhcp4_client + from ether.phrx import _phrx_ether + from ether.phtx import _phtx_ether + from icmp4.phrx import _phrx_icmp4 + from icmp4.phtx import _phtx_icmp4 + from icmp6.phrx import _phrx_icmp6 + from icmp6.phtx import _phtx_icmp6 + from ip4.phrx import _defragment_ip4_packet, _phrx_ip4 + from ip4.phtx import _phtx_ip4, _validate_dst_ip4_address, _validate_src_ip4_address + from ip6.phrx import _phrx_ip6 + from ip6.phtx import _phtx_ip6, _validate_dst_ip6_address, _validate_src_ip6_address + from ip6_ext_frag.phrx import _defragment_ip6_packet, _phrx_ip6_ext_frag + from ip6_ext_frag.phtx import _phtx_ip6_ext_frag + from tcp.phrx import _phrx_tcp + from tcp.phtx import _phtx_tcp + from udp.phrx import _phrx_udp + from udp.phtx import _phtx_udp def __init__(self, tap): """Class constructor""" + # Skip most of the initialisations for the unit test / mock run + if tap is None: + return + stack.packet_handler = self if __debug__: @@ -105,8 +97,8 @@ def __init__(self, tap): self.rx_ring = RxRing(tap) self.tx_ring = TxRing(tap) - self.arp_cache = ArpCache(self) - self.icmp6_nd_cache = ICMPv6NdCache(self) + self.arp_cache = ArpCache() + self.icmp6_nd_cache = NdCache() # Used for the ARP DAD process self.arp_probe_unicast_conflict = set() @@ -164,6 +156,7 @@ def __init__(self, tap): self._logger.info(f"Stack listening on multicast IPv4 addresses: {[str(_) for _ in self.ip4_multicast]}") self._logger.info(f"Stack listening on broadcast IPv4 addresses: {[str(_) for _ in self.ip4_broadcast]}") + # typing: Typing causes MyPy 0.812 to crash def __thread_packet_handler(self): """Thread picks up incoming packets from RX ring and processes them""" @@ -171,26 +164,26 @@ def __thread_packet_handler(self): self._phrx_ether(self.rx_ring.dequeue()) @property - def ip6_unicast(self): + def ip6_unicast(self) -> list[IPv6Address]: """Return list of stack's IPv6 unicast addresses""" return [_.ip for _ in self.ip6_address] @property - def ip4_unicast(self): + def ip4_unicast(self) -> list[IPv4Address]: """Return list of stack's IPv4 unicast addresses""" return [_.ip for _ in self.ip4_address] @property - def ip4_broadcast(self): + def ip4_broadcast(self) -> list[IPv4Address]: """Return list of stack's IPv4 broadcast addresses""" ip4_broadcast = [_.network.broadcast_address for _ in self.ip4_address] ip4_broadcast.append("255.255.255.255") return ip4_broadcast - def _perform_ip6_nd_dad(self, ip6_unicast_candidate): + def _perform_ip6_nd_dad(self, ip6_unicast_candidate: IPv6Address) -> bool: """Perform IPv6 ND Duplicate Address Detection, return True if passed""" if __debug__: @@ -202,47 +195,51 @@ def _perform_ip6_nd_dad(self, ip6_unicast_candidate): if __debug__: self._logger.warning(f"ICMPv6 ND DAD - Duplicate IPv6 address detected, {ip6_unicast_candidate} advertised by {self.icmp6_nd_dad_tlla}") else: - self._logger.debug(f"ICMPv6 ND DAD - No duplicate address detected for {ip6_unicast_candidate}") + if __debug__: + self._logger.debug(f"ICMPv6 ND DAD - No duplicate address detected for {ip6_unicast_candidate}") self.ip6_unicast_candidate = None self._remove_ip6_multicast(ip6_unicast_candidate.solicited_node_multicast) return not event - def _parse_stack_ip6_address_candidate(self, configured_address_candidate): + def _parse_stack_ip6_address_candidate(self, configured_address_candidate: list[tuple[str, str]]) -> list[IPv6Interface]: """Parse IPv6 candidate address list""" - valid_address_candidate = [] + valid_address_candidate: list[IPv6Interface] = [] - for address, gateway in configured_address_candidate: + for str_address, str_gateway in configured_address_candidate: if __debug__: - self._logger.debug(f"Parsing ('{address}', '{gateway}') entry") + self._logger.debug(f"Parsing ('{str_address}', '{str_gateway}') entry") try: - address = IPv6Interface(address) + address = IPv6Interface(str_address) except AddressValueError: if __debug__: - self._logger.warning(f"Invalid host address '{address}' format, skipping...") - return None + self._logger.warning(f"Invalid host address '{str_address}' format, skipping...") + continue if address.is_multicast or address.is_reserved or address.is_loopback or address.is_unspecified: if __debug__: self._logger.warning(f"Invalid host address '{address.ip}' type, skipping...") - return None + continue if address.ip in [_.ip for _ in valid_address_candidate]: if __debug__: self._logger.warning(f"Duplicate host address '{address.ip}' configured, skipping...") - return None - if gateway is not None: + continue + if address.is_link_local and str_gateway: + if __debug__: + self._logger.warning("Gateway cannot be configured for link local address skipping...") + continue + if str_gateway: try: - gateway = IPv6Address(gateway) + gateway: Optional[IPv6Address] = IPv6Address(str_gateway) + gateway = cast(IPv6Address, gateway) if not (gateway.is_link_local or (gateway in address.network and gateway != address.ip)): if __debug__: self._logger.warning(f"Invalid gateway '{gateway}' configured for interface address '{address}', skipping...") - gateway = None + continue except AddressValueError: if __debug__: - self._logger.warning(f"Invalid gateway '{gateway}' format configured for interface address '{address}' skipping...") - gateway = None - if address.is_link_local and gateway is not None: - if __debug__: - self._logger.warning("Gateway cannot be configured for link local address skipping...") + self._logger.warning(f"Invalid gateway '{str_gateway}' format configured for interface address '{address}' skipping...") + continue + else: gateway = None address.gateway = gateway valid_address_candidate.append(address) @@ -251,7 +248,7 @@ def _parse_stack_ip6_address_candidate(self, configured_address_candidate): return valid_address_candidate - def _create_stack_ip6_addressing(self): + def _create_stack_ip6_addressing(self) -> None: """Create lists of IPv6 unicast and multicast addresses stack should listen on""" def __(ip6_address): @@ -298,19 +295,19 @@ def __(ip6_address): ip6_address.gateway = gateway __(ip6_address) - def _parse_stack_ip4_address_candidate(self, configured_ip4_address_candidate): - """Parse IPv4 candidate addresses configured in stack.py module""" + def _parse_stack_ip4_address_candidate(self, configured_ip4_address_candidate: list[tuple[str, str]]) -> list[IPv4Interface]: + """Parse IPv4 candidate addresses configured in config.py module""" - valid_address_candidate = [] + valid_address_candidate: list[IPv4Interface] = [] - for address, gateway in configured_ip4_address_candidate: + for str_address, str_gateway in configured_ip4_address_candidate: if __debug__: - self._logger.debug(f"Parsing ('{address}', '{gateway}') entry") + self._logger.debug(f"Parsing ('{str_address}', '{str_gateway}') entry") try: - address = IPv4Interface(address) + address = IPv4Interface(str_address) except AddressValueError: if __debug__: - self._logger.warning(f"Invalid host address '{address}' format, skipping...") + self._logger.warning(f"Invalid host address '{str_address}' format, skipping...") continue if address.is_multicast or address.is_reserved or address.is_loopback or address.is_unspecified: if __debug__: @@ -324,17 +321,19 @@ def _parse_stack_ip4_address_candidate(self, configured_ip4_address_candidate): if __debug__: self._logger.warning(f"Duplicate host address '{address.ip}' configured, skipping...") continue - if gateway is not None: + if str_gateway: try: - gateway = IPv4Address(gateway) + gateway: Optional[IPv4Address] = IPv4Address(str_gateway) if gateway not in address.network or gateway == address.network_address or gateway == address.broadcast_address or gateway == address.ip: if __debug__: self._logger.warning(f"Invalid gateway '{gateway}' configured for interface address '{address}', skipping...") - gateway = None + continue except AddressValueError: if __debug__: - self._logger.warning(f"Invalid gateway '{gateway}' format configured for interface address '{address}' skipping...") - gateway = None + self._logger.warning(f"Invalid gateway '{str_gateway}' format configured for interface address '{address}' skipping...") + continue + else: + gateway = None address.gateway = gateway valid_address_candidate.append(address) if __debug__: @@ -342,7 +341,7 @@ def _parse_stack_ip4_address_candidate(self, configured_ip4_address_candidate): return valid_address_candidate - def _create_stack_ip4_addressing(self): + def _create_stack_ip4_addressing(self) -> None: """Create lists of IPv4 unicast, multicast and broadcast addresses stack should listen on""" # Perform Duplicate Address Detection @@ -373,13 +372,14 @@ def _create_stack_ip4_addressing(self): config.ip4_support = False return + # typing: Typing causes MyPy 0.812 to crash def _send_arp_probe(self, ip4_unicast): """Send out ARP probe to detect possible IP conflict""" self._phtx_arp( ether_src=self.mac_unicast, ether_dst="ff:ff:ff:ff:ff:ff", - arp_oper=fpa_arp.ARP_OP_REQUEST, + arp_oper=arp.ps.OP_REQUEST, arp_sha=self.mac_unicast, arp_spa=IPv4Address("0.0.0.0"), arp_tha="00:00:00:00:00:00", @@ -388,13 +388,14 @@ def _send_arp_probe(self, ip4_unicast): if __debug__: self._logger.debug(f"Sent out ARP probe for {ip4_unicast}") + # typing: Typing causes MyPy 0.812 to crash def _send_arp_announcement(self, ip4_unicast): """Send out ARP announcement to claim IP address""" self._phtx_arp( ether_src=self.mac_unicast, ether_dst="ff:ff:ff:ff:ff:ff", - arp_oper=fpa_arp.ARP_OP_REQUEST, + arp_oper=arp.ps.OP_REQUEST, arp_sha=self.mac_unicast, arp_spa=ip4_unicast, arp_tha="00:00:00:00:00:00", @@ -403,13 +404,14 @@ def _send_arp_announcement(self, ip4_unicast): if __debug__: self._logger.debug(f"Sent out ARP Announcement for {ip4_unicast}") + # typing: Typing causes MyPy 0.812 to crash def _send_gratitous_arp(self, ip4_unicast): """Send out gratitous arp""" self._phtx_arp( ether_src=self.mac_unicast, ether_dst="ff:ff:ff:ff:ff:ff", - arp_oper=fpa_arp.ARP_OP_REPLY, + arp_oper=arp.ps.OP_REPLY, arp_sha=self.mac_unicast, arp_spa=ip4_unicast, arp_tha="00:00:00:00:00:00", @@ -418,13 +420,14 @@ def _send_gratitous_arp(self, ip4_unicast): if __debug__: self._logger.debug(f"Sent out Gratitous ARP for {ip4_unicast}") + # typing: Typing causes MyPy 0.812 to crash def _send_icmp6_multicast_listener_report(self): """Send out ICMPv6 Multicast Listener Report for given list of addresses""" # Need to use set here to avoid re-using duplicate multicast entries from stack_ip6_multicast list, # also All Multicast Nodes address is not being advertised as this is not necessary if icmp6_mlr2_multicast_address_record := { - fpa_icmp6.MulticastAddressRecord(record_type=fpa_icmp6.ICMP6_MART_CHANGE_TO_EXCLUDE, multicast_address=str(_)) + icmp6.fpa.MulticastAddressRecord(record_type=icmp6.ps.MART_CHANGE_TO_EXCLUDE, multicast_address=str(_)) for _ in self.ip6_multicast if _ not in {IPv6Address("ff02::1")} }: @@ -432,7 +435,7 @@ def _send_icmp6_multicast_listener_report(self): ip6_src=self.ip6_unicast[0] if self.ip6_unicast else IPv6Address("::"), ip6_dst=IPv6Address("ff02::16"), ip6_hop=1, - icmp6_type=fpa_icmp6.ICMP6_MLD2_REPORT, + icmp6_type=icmp6.ps.MLD2_REPORT, icmp6_mlr2_multicast_address_record=icmp6_mlr2_multicast_address_record, ) if __debug__: @@ -440,6 +443,7 @@ def _send_icmp6_multicast_listener_report(self): f"Sent out ICMPv6 Multicast Listener Report message for {[_.multicast_address for _ in icmp6_mlr2_multicast_address_record]}" ) + # typing: Typing causes MyPy 0.812 to crash def _send_icmp6_nd_dad_message(self, ip6_unicast_candidate): """Send out ICMPv6 ND Duplicate Address Detection message""" @@ -447,12 +451,13 @@ def _send_icmp6_nd_dad_message(self, ip6_unicast_candidate): ip6_src=IPv6Address("::"), ip6_dst=ip6_unicast_candidate.solicited_node_multicast, ip6_hop=255, - icmp6_type=fpa_icmp6.ICMP6_NEIGHBOR_SOLICITATION, + icmp6_type=icmp6.ps.NEIGHBOR_SOLICITATION, icmp6_ns_target_address=ip6_unicast_candidate, ) if __debug__: self._logger.debug(f"Sent out ICMPv6 ND DAD message for {ip6_unicast_candidate}") + # typing: Typing causes MyPy 0.812 to crash def _send_icmp6_nd_router_solicitation(self): """Send out ICMPv6 ND Router Solicitation""" @@ -460,13 +465,14 @@ def _send_icmp6_nd_router_solicitation(self): ip6_src=self.ip6_unicast[0], ip6_dst=IPv6Address("ff02::2"), ip6_hop=255, - icmp6_type=fpa_icmp6.ICMP6_ROUTER_SOLICITATION, - icmp6_nd_options=[fpa_icmp6.Icmp6NdOptSLLA(self.mac_unicast)], + icmp6_type=icmp6.ps.ROUTER_SOLICITATION, + icmp6_nd_options=[icmp6.fpa.NdOptSLLA(self.mac_unicast)], ) + if __debug__: self._logger.debug("Sent out ICMPv6 ND Router Solicitation") - def _assign_ip6_address(self, ip6_address): + def _assign_ip6_address(self, ip6_address: IPv6Address) -> None: """Assign IPv6 unicast address to the list stack listens on""" self.ip6_address.append(ip6_address) @@ -474,7 +480,7 @@ def _assign_ip6_address(self, ip6_address): self._logger.debug(f"Assigned IPv6 unicast address {ip6_address}") self._assign_ip6_multicast(ip6_address.solicited_node_multicast) - def _remove_ip6_address(self, ip6_address): + def _remove_ip6_address(self, ip6_address: IPv6Address) -> None: """Remove IPv6 unicast address from the list stack listens on""" self.ip6_address.remove(ip6_address) @@ -482,7 +488,7 @@ def _remove_ip6_address(self, ip6_address): self._logger.debug(f"Removed IPv6 unicast address {ip6_address}") self._remove_ip6_multicast(ip6_address.solicited_node_multicast) - def _assign_ip6_multicast(self, ip6_multicast): + def _assign_ip6_multicast(self, ip6_multicast: IPv6Address) -> None: """Assign IPv6 multicast address to the list stack listens on""" self.ip6_multicast.append(ip6_multicast) @@ -492,7 +498,7 @@ def _assign_ip6_multicast(self, ip6_multicast): for _ in range(1): self._send_icmp6_multicast_listener_report() - def _remove_ip6_multicast(self, ip6_multicast): + def _remove_ip6_multicast(self, ip6_multicast: IPv6Address) -> None: """Remove IPv6 multicast address from the list stack listens on""" self.ip6_multicast.remove(ip6_multicast) @@ -500,111 +506,16 @@ def _remove_ip6_multicast(self, ip6_multicast): self._logger.debug(f"Removed IPv6 multicast {ip6_multicast}") self._remove_mac_multicast(ip6_multicast.multicast_mac) - def _assign_mac_multicast(self, mac_multicast): + def _assign_mac_multicast(self, mac_multicast: str) -> None: """Assign MAC multicast address to the list stack listens on""" self.mac_multicast.append(mac_multicast) if __debug__: self._logger.debug(f"Assigned MAC multicast {mac_multicast}") - def _remove_mac_multicast(self, mac_multicast): + def _remove_mac_multicast(self, mac_multicast: str) -> None: """Remove MAC multicast address from the list stack listens on""" self.mac_multicast.remove(mac_multicast) if __debug__: self._logger.debug(f"Removed MAC multicast {mac_multicast}") - - def _dhcp4_client(self): - """Obtain IPv4 address and default gateway using DHCP""" - - def _send_dhcp_packet(dhcp_packet_tx): - socket.send_to( - UdpMetadata( - local_ip_address=IPv4Address("0.0.0.0"), - local_port=68, - remote_ip_address=IPv4Address("255.255.255.255"), - remote_port=67, - data=dhcp_packet_tx.get_raw_packet(), - ) - ) - - socket = UdpSocket() - socket.bind(local_ip_address="0.0.0.0", local_port=68) - dhcp_xid = random.randint(0, 0xFFFFFFFF) - - # Send DHCP Discover - _send_dhcp_packet( - dhcp_packet_tx=ps_dhcp.DhcpPacket( - dhcp_xid=dhcp_xid, - dhcp_chaddr=self.mac_unicast, - dhcp_msg_type=ps_dhcp.DHCP_DISCOVER, - dhcp_param_req_list=b"\x01\x1c\x02\x03\x0f\x06\x77\x0c\x2c\x2f\x1a\x79\x2a", - dhcp_host_name="PyTCP", - ) - ) - if __debug__: - self._logger.debug("Sent out DHCP Discover message") - - # Wait for DHCP Offer - if not (packet := socket.receive_from(timeout=5)): - if __debug__: - self._logger.warning("Timeout waiting for DHCP Offer message") - socket.close() - return None, None - - dhcp_packet_rx = ps_dhcp.DhcpPacket(packet.data) - if dhcp_packet_rx.dhcp_msg_type != ps_dhcp.DHCP_OFFER: - if __debug__: - self._logger.warning("Didn't receive DHCP Offer message") - socket.close() - return None, None - - dhcp_srv_id = dhcp_packet_rx.dhcp_srv_id - dhcp_yiaddr = dhcp_packet_rx.dhcp_yiaddr - if __debug__: - self._logger.debug( - 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 - _send_dhcp_packet( - dhcp_packet_tx=ps_dhcp.DhcpPacket( - dhcp_xid=dhcp_xid, - dhcp_chaddr=self.mac_unicast, - dhcp_msg_type=ps_dhcp.DHCP_REQUEST, - dhcp_srv_id=dhcp_srv_id, - dhcp_req_ip4_addr=dhcp_yiaddr, - dhcp_param_req_list=b"\x01\x1c\x02\x03\x0f\x06\x77\x0c\x2c\x2f\x1a\x79\x2a", - dhcp_host_name="PyTCP", - ) - ) - - if __debug__: - self._logger.debug(f"Sent out DHCP Request message to {dhcp_packet_rx.dhcp_srv_id}") - - # Wait for DHCP Ack - if not (packet := socket.receive_from(timeout=5)): - if __debug__: - self._logger.warning("Timeout waiting for DHCP Ack message") - return None, None - - dhcp_packet_rx = ps_dhcp.DhcpPacket(packet.data) - if dhcp_packet_rx.dhcp_msg_type != ps_dhcp.DHCP_ACK: - if __debug__: - self._logger.warning("Didn't receive DHCP Offer message") - socket.close() - return None, None - - if __debug__: - self._logger.debug( - 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}" - ) - socket.close() - return ( - IPv4Interface(str(dhcp_packet_rx.dhcp_yiaddr) + "/" + str(IPv4Address._make_netmask(str(dhcp_packet_rx.dhcp_subnet_mask))[1])), - dhcp_packet_rx.dhcp_router[0], - ) diff --git a/rx_ring.py b/misc/rx_ring.py similarity index 68% rename from rx_ring.py rename to misc/rx_ring.py index d5ec9d73..e90103f0 100755 --- a/rx_ring.py +++ b/misc/rx_ring.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# rx_ring.py - module contains class supporting RX operations +# misc/rx_ring.py - module contains class supporting RX operations # @@ -46,17 +34,17 @@ import loguru -from packet import PacketRx +from misc.packet import PacketRx class RxRing: """Support for receiving packets from the network""" - def __init__(self, tap): + def __init__(self, tap: int) -> None: """Initialize access to tap interface and the inbound queue""" self.tap = tap - self.rx_ring = [] + self.rx_ring: list[PacketRx] = [] if __debug__: self._logger = loguru.logger.bind(object_name="rx_ring.") self.packet_enqueued = threading.Semaphore(0) @@ -65,7 +53,7 @@ def __init__(self, tap): if __debug__: self._logger.debug("Started RX ring") - def __thread_receive(self): + def __thread_receive(self) -> None: """Thread responsible for receiving and enqueuing incoming packets""" while True: @@ -75,7 +63,7 @@ def __thread_receive(self): self.rx_ring.append(packet_rx) self.packet_enqueued.release() - def dequeue(self): + def dequeue(self) -> PacketRx: """Dequeue inboutd frame from RX ring""" self.packet_enqueued.acquire() diff --git a/stack.py b/misc/stack.py similarity index 62% rename from stack.py rename to misc/stack.py index 2f2da9c8..3ed9b864 100755 --- a/stack.py +++ b/misc/stack.py @@ -23,26 +23,19 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # # stack.py - module holds references to the stack components and global structures # +from typing import Optional + +import tcp.session +import udp.socket +from misc.timer import Timer -timer = None +timer: Optional[Timer] = None packet_handler = None -tcp_sessions: dict = {} -udp_sockets: dict = {} +tcp_sessions: dict[str, tcp.session.TcpSession] = {} +udp_sockets: dict[str, udp.socket.UdpSocket] = {} diff --git a/stack_cli_server.py b/misc/stack_cli_server.py similarity index 83% rename from stack_cli_server.py rename to misc/stack_cli_server.py index c9925062..f742efc7 100755 --- a/stack_cli_server.py +++ b/misc/stack_cli_server.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# cli_server.py - module contains class suppoting stack's CLI funcionality +# misc/cli_server.py - module contains class suppoting stack's CLI funcionality # import socket @@ -45,7 +33,7 @@ import loguru -import stack +import misc.stack as stack class StackCliServer: diff --git a/timer.py b/misc/timer.py similarity index 73% rename from timer.py rename to misc/timer.py index 28d2ade8..4bfc9c26 100755 --- a/timer.py +++ b/misc/timer.py @@ -23,36 +23,25 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# timer.py - module contains class supporting timer that can be used by other stack components +# misc/timer.py - module contains class supporting timer that can be used by other stack components # import threading import time +from typing import Callable, Optional import loguru -import stack +import misc.stack as stack class TimerTask: """Timer task support class""" - def __init__(self, method, args, kwargs, delay, delay_exp, repeat_count, stop_condition): + def __init__(self, method: Callable, args: list, kwargs: dict, delay: int, delay_exp: bool, repeat_count: int, stop_condition: Optional[Callable]) -> None: """Class constructor, repeat_count = -1 means infinite, delay_exp means to raise delay time exponentially after each method execution""" self.method = method @@ -90,7 +79,7 @@ def tick(self): class Timer: """Support for stack timer""" - def __init__(self): + def __init__(self) -> None: """Class constructor""" stack.timer = self @@ -100,14 +89,14 @@ def __init__(self): self.run_timer = True - self.tasks = [] - self.timers = {} + self.tasks: list[TimerTask] = [] + self.timers: dict[str, int] = {} threading.Thread(target=self.__thread_timer).start() if __debug__: self._logger.debug("Started timer") - def __thread_timer(self): + def __thread_timer(self) -> None: """Thread responsible for executing registered methods on every timer tick""" while self.run_timer: @@ -127,17 +116,26 @@ def __thread_timer(self): # Cleanup expired methods self.tasks = [_ for _ in self.tasks if _.remaining_delay] - def register_method(self, method, args=None, kwargs=None, delay=1, delay_exp=False, repeat_count=-1, stop_condition=None): + def register_method( + self, + method: Callable, + args: list = None, + kwargs: dict = None, + delay: int = 1, + delay_exp: bool = False, + repeat_count: int = -1, + stop_condition: Optional[Callable] = None, + ) -> None: """Register method to be executed by timer""" self.tasks.append(TimerTask(method, [] if args is None else args, {} if kwargs is None else kwargs, delay, delay_exp, repeat_count, stop_condition)) - def register_timer(self, name, timeout): + def register_timer(self, name: str, timeout: int) -> None: """Register delay timer""" self.timers[name] = timeout - def is_expired(self, name): + def is_expired(self, name: str) -> bool: """Check if timer expired""" if __debug__: diff --git a/tracker.py b/misc/tracker.py similarity index 70% rename from tracker.py rename to misc/tracker.py index e5c8f550..f2669e20 100755 --- a/tracker.py +++ b/misc/tracker.py @@ -23,25 +23,15 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# tracker.py - generate serial number information for new packets +# misc/tracker.py - generate serial number information for new packets # +from __future__ import annotations # Required for Python version lower than 3.10 import time +from typing import Optional class Tracker: @@ -50,7 +40,7 @@ class Tracker: serial_rx = 0 serial_tx = 0 - def __init__(self, prefix, echo_tracker=None): + def __init__(self, prefix: str, echo_tracker: Optional[Tracker] = None) -> None: """Class constructor""" self.echo_tracker = echo_tracker @@ -71,7 +61,7 @@ def __init__(self, prefix, echo_tracker=None): if Tracker.serial_tx > 0xFFFF: Tracker.serial_tx = 0 - def __str__(self): + def __str__(self) -> str: """Return serial number string""" if self.echo_tracker: @@ -80,7 +70,7 @@ def __str__(self): return self.serial @property - def latency(self): + def latency(self) -> str: """Latency between echo tracker timestamp and current time""" if self.echo_tracker: diff --git a/tx_ring.py b/misc/tx_ring.py similarity index 72% rename from tx_ring.py rename to misc/tx_ring.py index e315785d..c1e2364d 100755 --- a/tx_ring.py +++ b/misc/tx_ring.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# tx_ring.py - module contains class supporting TX operations +# misc/tx_ring.py - module contains class supporting TX operations # @@ -47,19 +35,20 @@ import loguru import config +from ether.fpa import Assembler as EtherAssembler class TxRing: """Support for sending packets to the network""" - def __init__(self, tap): + def __init__(self, tap: int) -> None: """Initialize access to tap interface and the outbound queue""" if __debug__: self._logger = loguru.logger.bind(object_name="tx_ring.") self.tap = tap - self.tx_ring = [] + self.tx_ring: list[EtherAssembler] = [] self.packet_enqueued = threading.Semaphore(0) @@ -69,7 +58,7 @@ def __init__(self, tap): self.frame = bytearray(config.mtu + 14) - def __thread_transmit(self): + def __thread_transmit(self) -> None: """Dequeue packet from TX ring and send it out""" while True: @@ -79,7 +68,7 @@ def __thread_transmit(self): if __debug__: self._logger.error(f"{packet_tx.tracker} - Unable to send frame, frame len ({packet_tx_len}) > mtu ({config.mtu + 14})") continue - packet_tx.assemble_packet(self.frame, 0) + packet_tx.assemble(self.frame, 0) try: os.write(self.tap, memoryview(self.frame)[:packet_tx_len]) @@ -92,7 +81,7 @@ def __thread_transmit(self): f"[TX] {packet_tx.tracker}{packet_tx.tracker.latency} - sent frame, {len(packet_tx)} bytes" ) - def enqueue(self, packet_tx): + def enqueue(self, packet_tx: EtherAssembler) -> None: """Enqueue outbound packet into TX ring""" self.tx_ring.append(packet_tx) diff --git a/pytcp.py b/pytcp.py index 4d6a2e68..bfceedfc 100755 --- a/pytcp.py +++ b/pytcp.py @@ -23,18 +23,6 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # # pytcp.py - main TCP/IP stack program @@ -50,17 +38,17 @@ import loguru import config -from client_icmp_echo import ClientIcmpEcho -from client_tcp_echo import ClientTcpEcho -from ph import PacketHandler -from service_tcp_daytime import ServiceTcpDaytime -from service_tcp_discard import ServiceTcpDiscard -from service_tcp_echo import ServiceTcpEcho -from service_udp_daytime import ServiceUdpDaytime -from service_udp_discard import ServiceUdpDiscard -from service_udp_echo import ServiceUdpEcho -from stack_cli_server import StackCliServer -from timer import Timer +from client.icmp_echo import ClientIcmpEcho +from client.tcp_echo import ClientTcpEcho +from misc.ph import PacketHandler +from misc.stack_cli_server import StackCliServer +from misc.timer import Timer +from service.tcp_daytime import ServiceTcpDaytime +from service.tcp_discard import ServiceTcpDiscard +from service.tcp_echo import ServiceTcpEcho +from service.udp_daytime import ServiceUdpDaytime +from service.udp_discard import ServiceUdpDiscard +from service.udp_echo import ServiceUdpEcho TUNSETIFF = 0x400454CA IFF_TAP = 0x0002 @@ -77,7 +65,7 @@ ######################################################### -def main(): +def main() -> int: """Main function""" loguru.logger.remove(0) @@ -101,6 +89,7 @@ def main(): try: tap = os.open("/dev/net/tun", os.O_RDWR) + except FileNotFoundError: _logger.error("Unable to access '/dev/net/tun' device") sys.exit(-1) @@ -134,16 +123,16 @@ def main(): if config.service_tcp_discard: ServiceTcpDiscard(local_ip_address=local_ip_address) if config.service_tcp_daytime: - ServiceTcpDaytime(local_ip_address=local_ip_address, message_count=-1, message_delay=1, message_size=1000) + ServiceTcpDaytime(local_ip_address=local_ip_address, message_count=-1, message_delay=1, message_size=5) # Initialize TCP test clients if config.client_tcp_echo: - ClientTcpEcho(local_ip_address="192.168.9.7", remote_ip_address="192.168.100.102", remote_port=7, message_count=10) + ClientTcpEcho(local_ip_address="192.168.9.7", remote_ip_address="192.168.9.102", remote_port=7, message_count=10) ClientTcpEcho( local_ip_address="fdd1:c296:f24f:9:0:ff:fe77:7777", remote_ip_address="fdd1:c296:f24f:9:5054:ff:fedf:8537", remote_port=7, message_count=10 ) - # ClientTcpEcho(local_ip_address="192.168.9.7", remote_ip_address="1.1.1.1", remote_port=7) - # ClientTcpEcho(local_ip_address="192.168.9.7", remote_ip_address="192.168.9.9", remote_port=7) + ClientTcpEcho(local_ip_address="192.168.9.7", remote_ip_address="1.1.1.1", remote_port=7) + ClientTcpEcho(local_ip_address="192.168.9.7", remote_ip_address="192.168.9.9", remote_port=7) # Initialize ICMP test client if config.client_icmp_echo: @@ -161,7 +150,7 @@ def main(): # Another subnet, source address not specified # ClientIcmpEcho(local_ip_address="::", remote_ip_address="fdd1:c296:f24f:100:5054:ff:fef9:99aa", message_count=10) - ClientIcmpEcho(local_ip_address="0.0.0.0", remote_ip_address="8.8.8.8", message_count=25) + ClientIcmpEcho(local_ip_address="0.0.0.0", remote_ip_address="8.8.8.8", message_count=5) ClientIcmpEcho(local_ip_address="::", remote_ip_address="2001:4860:4860::8888", message_count=25) # Another subnet, source with no default gateway assigned @@ -170,6 +159,8 @@ def main(): while True: time.sleep(1) + return 0 + if __name__ == "__main__": sys.exit(main()) diff --git a/requirements.txt b/requirements.txt index 89ee7dc8..4ba2b84f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -black==21.5b1 -codespell==2.0.0 -flake8==3.9.2 -loguru==0.5.3 -isort==5.8.0 -mypy==0.812 -mypy-extensions==0.4.3 +loguru +testslide +black +mypy +flake8 +codespell +isort diff --git a/service/__init__.py b/service/__init__.py new file mode 100755 index 00000000..1e0d382c --- /dev/null +++ b/service/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# service/__init__.py +# diff --git a/service_tcp_daytime.py b/service/tcp_daytime.py similarity index 62% rename from service_tcp_daytime.py rename to service/tcp_daytime.py index cf45fb24..9e4bd851 100755 --- a/service_tcp_daytime.py +++ b/service/tcp_daytime.py @@ -23,28 +23,16 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# service_tcp_daytime.py - 'user space' service TCP Daytime (RFC 867) +# service/tcp_daytime.py - 'user space' service TCP Daytime (RFC 867) # import threading import time -import tcp_socket +from tcp.socket import TcpSocket # from datetime import datetime @@ -52,41 +40,49 @@ class ServiceTcpDaytime: """TCP Daytime service support class""" - def __init__(self, local_ip_address="*", local_port=13, message_count=1, message_delay=0, message_size=1): + def __init__(self, local_ip_address: str = "*", local_port: int = 13, message_count: int = 10, message_delay: int = 0, message_size: int = 1): """Class constructor""" - threading.Thread(target=self.__thread_service, args=(local_ip_address, local_port, message_count, message_delay, message_size)).start() + self.local_ip_address = local_ip_address + self.local_port = local_port + self.message_count = message_count + self.message_delay = message_delay + self.message_size = message_size + + threading.Thread(target=self.__thread_service).start() - def __thread_service(self, local_ip_address, local_port, message_count, message_delay, message_size): + def __thread_service(self) -> None: """Service initialization""" - socket = tcp_socket.TcpSocket() - socket.bind(local_ip_address, local_port) + socket = TcpSocket() + socket.bind(self.local_ip_address, self.local_port) socket.listen() - print(f"Service TCP Daytime: Socket created, bound to {local_ip_address}, port {local_port} and set to listening mode") + print(f"Service TCP Daytime: Socket created, bound to {self.local_ip_address}, port {self.local_port} and set to listening mode") while True: new_socket = socket.accept() print(f"Service TCP Daytime: Inbound connection received from {new_socket.remote_ip_address}, port {new_socket.remote_port}") - threading.Thread(target=self.__thread_connection, args=(new_socket, message_count, message_delay, message_size)).start() + threading.Thread(target=self.__thread_connection, args=(new_socket,)).start() - @staticmethod - def __thread_connection(socket, message_count, message_delay, message_size): + def __thread_connection(self, socket: TcpSocket) -> None: """Inbound connection handler""" + # Don't want to be working on object variable as it may be shared by multiple connections + message_count = self.message_count + while message_count: - # daytime = "bytes(str(datetime.now()) + "\n", "utf-8") * message_size + # daytime = "bytes(str(datetime.now()) + "\n", "utf-8") * self.message_size message = "[------START------] " - for i in range(message_size - 2): + for i in range(self.message_size - 2): message += f"[------{i + 1:05}------] " message += "[-------END-------]\n" daytime = bytes(message, "utf-8") if result := socket.send(daytime): print(f"Service TCP Daytime: Sent daytime message to {socket.remote_ip_address}, port {socket.remote_port}") - time.sleep(message_delay) + time.sleep(self.message_delay) message_count = min(message_count, message_count - 1) if result == -1: print(f"Service TCP Daytime: Connection to {socket.remote_ip_address}, port {socket.remote_port} has been closed by remote peer") diff --git a/service_tcp_discard.py b/service/tcp_discard.py similarity index 60% rename from service_tcp_discard.py rename to service/tcp_discard.py index 43c2e9be..a5ac40b6 100755 --- a/service_tcp_discard.py +++ b/service/tcp_discard.py @@ -23,44 +23,35 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# service_tcp_discard.py - 'user space' service TCP Discard (RFC 863) +# service/tcp_discard.py - 'user space' service TCP Discard (RFC 863) # import threading -import tcp_socket +from tcp.socket import TcpSocket class ServiceTcpDiscard: """TCP Discard service support class""" - def __init__(self, local_ip_address="*", local_port=9): + def __init__(self, local_ip_address: str = "*", local_port: int = 9) -> None: """Class constructor""" - threading.Thread(target=self.__thread_service, args=(local_ip_address, local_port)).start() + self.local_ip_address = local_ip_address + self.local_port = local_port + + threading.Thread(target=self.__thread_service).start() - def __thread_service(self, local_ip_address, local_port): + def __thread_service(self) -> None: """Service initialization""" - socket = tcp_socket.TcpSocket() - socket.bind(local_ip_address, local_port) + socket = TcpSocket() + socket.bind(self.local_ip_address, self.local_port) socket.listen() - print(f"Service TCP Discard: Socket created, bound to {local_ip_address}, port {local_port} and set to listening mode") + print(f"Service TCP Discard: Socket created, bound to {self.local_ip_address}, port {self.local_port} and set to listening mode") while True: new_socket = socket.accept() @@ -68,8 +59,7 @@ def __thread_service(self, local_ip_address, local_port): threading.Thread(target=self.__thread_connection, args=(new_socket,)).start() - @staticmethod - def __thread_connection(socket): + def __thread_connection(self, socket: TcpSocket) -> None: """Inbound connection handler""" while True: @@ -78,7 +68,7 @@ def __thread_connection(socket): if message is None: break - print(f"Service TCP Discard: Discarded message from {socket.remote_ip_address}, port {socket.remote_port} -", message) + print(f"Service TCP Discard: Discarded message from {socket.remote_ip_address}, port {socket.remote_port}, {len(message)}") socket.close() print(f"Service TCP Discard: Connection from {socket.remote_ip_address}, port {socket.remote_port} has been closed by peer") diff --git a/service_tcp_echo.py b/service/tcp_echo.py similarity index 70% rename from service_tcp_echo.py rename to service/tcp_echo.py index c3b53409..06181653 100755 --- a/service_tcp_echo.py +++ b/service/tcp_echo.py @@ -23,45 +23,36 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# service_tcp_echo.py - 'user space' service TCP Echo (RFC 862) +# service/tcp_echo.py - 'user space' service TCP Echo (RFC 862) # import threading -import tcp_socket -from malpi import malpa, malpi, malpka +from misc.malpi import malpa, malpi, malpka +from tcp.socket import TcpSocket class ServiceTcpEcho: """TCP Echo service support class""" - def __init__(self, local_ip_address="*", local_port=7): + def __init__(self, local_ip_address: str = "*", local_port: int = 7) -> None: """Class constructor""" - threading.Thread(target=self.__thread_service, args=(local_ip_address, local_port)).start() + self.local_ip_address = local_ip_address + self.local_port = local_port + + threading.Thread(target=self.__thread_service).start() - def __thread_service(self, local_ip_address, local_port): + def __thread_service(self) -> None: """Service initialization""" - socket = tcp_socket.TcpSocket() - socket.bind(local_ip_address, local_port) + socket = TcpSocket() + socket.bind(self.local_ip_address, self.local_port) socket.listen() - print(f"Service TCP Echo: Socket created, bound to {local_ip_address}, port {local_port} and set to listening mode") + print(f"Service TCP Echo: Socket created, bound to {self.local_ip_address}, port {self.local_port} and set to listening mode") while True: new_socket = socket.accept() @@ -69,8 +60,7 @@ def __thread_service(self, local_ip_address, local_port): threading.Thread(target=self.__thread_connection, args=(new_socket,)).start() - @staticmethod - def __thread_connection(socket): + def __thread_connection(self, socket: TcpSocket) -> None: """Inbound connection handler""" print(f"Service TCP Echo: Sending first message to {socket.remote_ip_address}, port {socket.remote_port}") diff --git a/service_udp_daytime.py b/service/udp_daytime.py similarity index 60% rename from service_udp_daytime.py rename to service/udp_daytime.py index ce4aab74..e0565bf2 100755 --- a/service_udp_daytime.py +++ b/service/udp_daytime.py @@ -23,47 +23,37 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# service_udp_daytime.py - 'user space' service UDP Daytime (RFC 867) +# service/udp_daytime.py - 'user space' service UDP Daytime (RFC 867) # import threading from datetime import datetime -import udp_socket -from tracker import Tracker -from udp_metadata import UdpMetadata +from misc.tracker import Tracker +from udp.metadata import UdpMetadata +from udp.socket import UdpSocket class ServiceUdpDaytime: """UDP Daytime service support class""" - def __init__(self, local_ip_address="*", local_port=13): + def __init__(self, local_ip_address: str = "*", local_port: int = 13) -> None: """Class constructor""" - threading.Thread(target=self.__thread_service, args=(local_ip_address, local_port)).start() + self.local_ip_address = local_ip_address + self.local_port = local_port + + threading.Thread(target=self.__thread_service).start() - @staticmethod - def __thread_service(local_ip_address, local_port): + def __thread_service(self) -> None: """Service initialization and rx/tx loop""" - socket = udp_socket.UdpSocket() - socket.bind(local_ip_address, local_port) - print(f"Service UDP Daytime: Socket created, bound to {local_ip_address}, port {local_port}") + socket = UdpSocket() + socket.bind(self.local_ip_address, self.local_port) + print(f"Service UDP Daytime: Socket created, bound to {self.local_ip_address}, port {self.local_port}") while True: packet_rx = socket.receive_from() @@ -72,7 +62,7 @@ def __thread_service(local_ip_address, local_port): local_port=packet_rx.local_port, remote_ip_address=packet_rx.remote_ip_address, remote_port=packet_rx.remote_port, - _data=bytes(str(datetime.now()), "utf-8"), + data=bytes(str(datetime.now()), "utf-8"), tracker=Tracker("TX", echo_tracker=packet_rx.tracker), ) socket.send_to(packet_tx) diff --git a/service_udp_discard.py b/service/udp_discard.py similarity index 57% rename from service_udp_discard.py rename to service/udp_discard.py index 49b3e2d2..daa9b4a5 100755 --- a/service_udp_discard.py +++ b/service/udp_discard.py @@ -23,44 +23,34 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# service_udp_discard.py - 'user space' service UDP Discard (RFC 863) +# service/udp_discard.py - 'user space' service UDP Discard (RFC 863) # import threading -import udp_socket +from udp.socket import UdpSocket class ServiceUdpDiscard: """UDP Discard service support class""" - def __init__(self, local_ip_address="*", local_port=9): + def __init__(self, local_ip_address: str = "*", local_port: int = 9) -> None: """Class constructor""" - threading.Thread(target=self.__thread_service, args=(local_ip_address, local_port)).start() + self.local_ip_address = local_ip_address + self.local_port = local_port + + threading.Thread(target=self.__thread_service).start() - @staticmethod - def __thread_service(local_ip_address, local_port): + def __thread_service(self) -> None: """Service initialization and rx/tx loop""" - socket = udp_socket.UdpSocket() - socket.bind(local_ip_address, local_port) - print(f"Service UDP Discard: Socket created, bound to {local_ip_address}, port {local_port}") + socket = UdpSocket() + socket.bind(self.local_ip_address, self.local_port) + print(f"Service UDP Discard: Socket created, bound to {self.local_ip_address}, port {self.local_port}") while True: packet_rx = socket.receive_from() diff --git a/service_udp_echo.py b/service/udp_echo.py similarity index 64% rename from service_udp_echo.py rename to service/udp_echo.py index 9e948f68..61be050e 100755 --- a/service_udp_echo.py +++ b/service/udp_echo.py @@ -23,47 +23,37 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# service_udp_echo.py - 'user space' service UDP Echo (RFC 862) +# service/udp_echo.py - 'user space' service UDP Echo (RFC 862) # import threading -import udp_socket -from malpi import malpa, malpi, malpka -from tracker import Tracker -from udp_metadata import UdpMetadata +from misc.malpi import malpa, malpi, malpka +from misc.tracker import Tracker +from udp.metadata import UdpMetadata +from udp.socket import UdpSocket class ServiceUdpEcho: """UDP Echo service support class""" - def __init__(self, local_ip_address="*", local_port=7): + def __init__(self, local_ip_address: str = "*", local_port: int = 7) -> None: """Class constructor""" - threading.Thread(target=self.__thread_service, args=(local_ip_address, local_port)).start() + self.local_ip_address = local_ip_address + self.local_port = local_port + + threading.Thread(target=self.__thread_service).start() - @staticmethod - def __thread_service(local_ip_address, local_port): + def __thread_service(self) -> None: """Service initialization and rx/tx loop""" - socket = udp_socket.UdpSocket() - socket.bind(local_ip_address, local_port) - print(f"Service UDP Echo: Socket created, bound to {local_ip_address}, port {local_port}") + socket = UdpSocket() + socket.bind(self.local_ip_address, self.local_port) + print(f"Service UDP Echo: Socket created, bound to {self.local_ip_address}, port {self.local_port}") while True: packet_rx = socket.receive_from() diff --git a/setup.cfg b/setup.cfg index 7b211a38..0b76338f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,6 @@ [flake8] max-line-length = 160 ignore = E203, W503, W605 + +[mypy] +check_untyped_defs = no diff --git a/tcp/__init__.py b/tcp/__init__.py new file mode 100755 index 00000000..3dc463f6 --- /dev/null +++ b/tcp/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# tcp/__init__.py +# diff --git a/tcp/fpa.py b/tcp/fpa.py new file mode 100755 index 00000000..be365893 --- /dev/null +++ b/tcp/fpa.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# tcp/fpa.py - Fast Packet Assembler support class for TCP protocol +# + + +import struct +from typing import Optional + +import ip4.ps +import ip6.ps +import tcp.ps +from misc.ip_helper import inet_cksum +from misc.tracker import Tracker + + +class Assembler: + """TCP packet assembler support class""" + + ip4_proto = ip4.ps.PROTO_TCP + ip6_next = ip6.ps.NEXT_HEADER_TCP + + def __init__( + self, + sport: int, + dport: int, + seq: int = 0, + ack: int = 0, + flag_ns: bool = False, + flag_crw: bool = False, + flag_ece: bool = False, + flag_urg: bool = False, + flag_ack: bool = False, + flag_psh: bool = False, + flag_rst: bool = False, + flag_syn: bool = False, + flag_fin: bool = False, + win: int = 0, + urp: int = 0, + options: Optional[list] = None, + data: Optional[bytes] = None, + echo_tracker: Optional[Tracker] = None, + ) -> None: + """Class constructor""" + + self.tracker = Tracker("TX", echo_tracker) + self.sport = sport + self.dport = dport + self.seq = seq + self.ack = ack + self.flag_ns = flag_ns + self.flag_crw = flag_crw + self.flag_ece = flag_ece + self.flag_urg = flag_urg + self.flag_ack = flag_ack + self.flag_psh = flag_psh + self.flag_rst = flag_rst + self.flag_syn = flag_syn + self.flag_fin = flag_fin + self.win = win + self.urp = urp + self.options = [] if options is None else options + self.data = b"" if data is None else data + self.hlen = tcp.ps.HEADER_LEN + sum([len(_) for _ in self.options]) + + assert self.hlen % 4 == 0, f"TCP header len {self.hlen} is not multiplcation of 4 bytes, check options... {self.options}" + + def __len__(self) -> int: + """Length of the packet""" + + return self.hlen + len(self.data) + + from tcp.ps import __str__ + + @property + def raw_options(self) -> bytes: + """Packet options in raw format""" + + raw_options = b"" + + for option in self.options: + raw_options += option.raw_option + + return raw_options + + def assemble(self, frame: bytearray, hptr: int, pshdr_sum: int): + """Assemble packet into the raw form""" + + struct.pack_into( + f"! HH L L BBH HH {len(self.raw_options)}s {len(self.data)}s", + frame, + hptr, + self.sport, + self.dport, + self.seq, + self.ack, + self.hlen << 2 | self.flag_ns, + self.flag_crw << 7 + | self.flag_ece << 6 + | self.flag_urg << 5 + | self.flag_ack << 4 + | self.flag_psh << 3 + | self.flag_rst << 2 + | self.flag_syn << 1 + | self.flag_fin, + self.win, + 0, + self.urp, + self.raw_options, + self.data, + ) + + struct.pack_into("! H", frame, hptr + 16, inet_cksum(frame, hptr, len(self), pshdr_sum)) + + +# +# TCP options +# + + +class OptEol(tcp.ps.OptEol): + """TCP option - End of Option List (0)""" + + @property + def raw_option(self) -> bytes: + return struct.pack("!B", tcp.ps.OPT_EOL) + + +class OptNop(tcp.ps.OptNop): + """TCP option - No Operation (1)""" + + @property + def raw_option(self) -> bytes: + return struct.pack("!B", tcp.ps.OPT_NOP) + + +class OptMss(tcp.ps.OptMss): + """TCP option - Maximum Segment Size (2)""" + + def __init__(self, mss: int) -> None: + self.mss = mss + + @property + def raw_option(self) -> bytes: + return struct.pack("! BB H", tcp.ps.OPT_MSS, tcp.ps.OPT_MSS_LEN, self.mss) + + +class OptWscale(tcp.ps.OptWscale): + """TCP option - Window Scale (3)""" + + def __init__(self, wscale: int) -> None: + self.wscale = wscale + + @property + def raw_option(self) -> bytes: + return struct.pack("! BB B", tcp.ps.OPT_WSCALE, tcp.ps.OPT_WSCALE_LEN, self.wscale) + + +class OptSackPerm(tcp.ps.OptSackPerm): + """TCP option - Sack Permit (4)""" + + @property + def raw_option(self) -> bytes: + return struct.pack("! BB", tcp.ps.OPT_SACKPERM, tcp.ps.OPT_SACKPERM_LEN) + + +class OptTimestamp(tcp.ps.OptTimestamp): + """TCP option - Timestamp (8)""" + + def __init__(self, tsval: int, tsecr: int) -> None: + self.tsval = tsval + self.tsecr = tsecr + + @property + def raw_option(self) -> bytes: + return struct.pack("! BB LL", tcp.ps.OPT_TIMESTAMP, tcp.ps.OPT_TIMESTAMP_LEN, self.tsval, self.tsecr) diff --git a/tcp/fpp.py b/tcp/fpp.py new file mode 100755 index 00000000..9feaf05e --- /dev/null +++ b/tcp/fpp.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# tcp/fpp.py - Fast Packet Parser support class for TCP protocol +# + + +import struct +from typing import Optional, cast + +import config +import tcp.ps +from ip6.fpp import Parser as Ip6Parser +from misc.ip_helper import inet_cksum +from misc.packet import PacketRx + + +class Parser: + """TCP packet parser class""" + + def __init__(self, packet_rx: PacketRx) -> None: + """Class constructor""" + + packet_rx.tcp = self + + self._frame = packet_rx.frame + self._hptr = packet_rx.hptr + + # Casting here does not matter if uses 6 or 4 + packet_rx.ip = cast(Ip6Parser, packet_rx.ip) + self._plen = packet_rx.ip.dlen + + packet_rx.parse_failed = self._packet_integrity_check(packet_rx.ip.pshdr_sum) or self._packet_sanity_check() + + if packet_rx.parse_failed: + packet_rx.hptr = self._hptr + self.hlen + + def __len__(self) -> int: + """Packet length""" + + return len(self._frame) - self._hptr + + from tcp.ps import __str__ + + @property + def sport(self) -> int: + """Read 'Source port' field""" + + if "_cache__sport" not in self.__dict__: + self._cache__sport = struct.unpack_from("!H", self._frame, self._hptr + 0)[0] + return self._cache__sport + + @property + def dport(self) -> int: + """Read 'Destianation port' field""" + + if "_cache__dport" not in self.__dict__: + self._cache__dport = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] + return self._cache__dport + + @property + def seq(self) -> int: + """Read 'Sequence number' field""" + + if "_cache__seq" not in self.__dict__: + self._cache__seq = struct.unpack_from("!L", self._frame, self._hptr + 4)[0] + return self._cache__seq + + @property + def ack(self) -> int: + """Read 'Acknowledge number' field""" + + if "_cache__ack" not in self.__dict__: + self._cache__ack = struct.unpack_from("!L", self._frame, self._hptr + 8)[0] + return self._cache__ack + + @property + def hlen(self) -> int: + """Read 'Header length' field""" + + if "_cache__hlen" not in self.__dict__: + self._cache__hlen = (self._frame[self._hptr + 12] & 0b11110000) >> 2 + return self._cache__hlen + + @property + def flag_ns(self) -> bool: + """Read 'NS flag' field""" + + if "_cache__flag_ns" not in self.__dict__: + self._cache__flag_ns = bool(self._frame[self._hptr + 12] & 0b00000001) + return self._cache__flag_ns + + @property + def flag_crw(self) -> bool: + """Read 'CRW flag' field""" + + if "_cache__flag_crw" not in self.__dict__: + self._cache__flag_crw = bool(self._frame[self._hptr + 13] & 0b10000000) + return self._cache__flag_crw + + @property + def flag_ece(self) -> bool: + """Read 'ECE flag' field""" + + if "_cache__flag_ece" not in self.__dict__: + self._cache__flag_ece = bool(self._frame[self._hptr + 13] & 0b01000000) + return self._cache__flag_ece + + @property + def flag_urg(self) -> bool: + """Read 'URG flag' field""" + + if "_cache__flag_urg" not in self.__dict__: + self._cache__flag_urg = bool(self._frame[self._hptr + 13] & 0b00100000) + return self._cache__flag_urg + + @property + def flag_ack(self) -> bool: + """Read 'ACK flag' field""" + + if "_cache__flag_ack" not in self.__dict__: + self._cache__flag_ack = bool(self._frame[self._hptr + 13] & 0b00010000) + return self._cache__flag_ack + + @property + def flag_psh(self) -> bool: + """Read 'PSH flag' field""" + + if "_cache__flag_psh" not in self.__dict__: + self._cache__flag_psh = bool(self._frame[self._hptr + 13] & 0b00001000) + return self._cache__flag_psh + + @property + def flag_rst(self) -> bool: + """Read 'RST flag' field""" + + if "_cache__flag_rst" not in self.__dict__: + self._cache__flag_rst = bool(self._frame[self._hptr + 13] & 0b00000100) + return self._cache__flag_rst + + @property + def flag_syn(self) -> bool: + """Read 'SYN flag' field""" + + if "_cache__flag_syn" not in self.__dict__: + self._cache__flag_syn = bool(self._frame[self._hptr + 13] & 0b00000010) + return self._cache__flag_syn + + @property + def flag_fin(self) -> bool: + """Read 'FIN flag' field""" + + if "_cache__flag_fin" not in self.__dict__: + self._cache__flag_fin = bool(self._frame[self._hptr + 13] & 0b00000001) + return self._cache__flag_fin + + @property + def win(self): + """Read 'Window' field""" + + if "_cache__win" not in self.__dict__: + self._cache__win = struct.unpack_from("!H", self._frame, self._hptr + 14)[0] + return self._cache__win + + @property + def cksum(self) -> int: + """Read 'Checksum' field""" + + if "_cache__cksum" not in self.__dict__: + self._cache__cksum = struct.unpack_from("!H", self._frame, self._hptr + 16)[0] + return self._cache__cksum + + @property + def urg(self) -> int: + """Read 'Urgent pointer' field""" + + if "_cache__urg" not in self.__dict__: + self._cache__urg = struct.unpack_from("!H", self._frame, self._hptr + 18)[0] + return self._cache__urg + + @property + def data(self) -> bytes: + """Read the data packet carries""" + + if "_cache__data" not in self.__dict__: + self._cache__data = self._frame[self._hptr + self.hlen : self._hptr + self.plen] + return self._cache__data + + @property + def olen(self) -> int: + """Calculate options length""" + + if "_cache__olen" not in self.__dict__: + self._cache__olen = self.hlen - tcp.ps.HEADER_LEN + return self._cache__olen + + @property + def dlen(self) -> int: + """Calculate data length""" + + return self._plen - self.hlen + + @property + def plen(self) -> int: + """Calculate packet length""" + + return self._plen + + @property + def header_copy(self) -> bytes: + """Return copy of packet header""" + + if "_cache__header_copy" not in self.__dict__: + self._cache__header_copy = self._frame[self._hptr : self._hptr + tcp.ps.HEADER_LEN] + return self._cache__header_copy + + @property + def options_copy(self) -> bytes: + """Return copy of packet header""" + + if "_cache__options_copy" not in self.__dict__: + self._cache__options_copy = self._frame[self._hptr + tcp.ps.HEADER_LEN : self._hptr + self.hlen] + return self._cache__options_copy + + @property + def data_copy(self) -> bytes: + """Return copy of packet data""" + + if "_cache__data_copy" not in self.__dict__: + self._cache__data_copy = self._frame[self._hptr + self.hlen : self._hptr + self.plen] + return self._cache__data_copy + + @property + def packet_copy(self) -> bytes: + """Return copy of whole packet""" + + if "_cache__packet_copy" not in self.__dict__: + self._cache__packet_copy = self._frame[self._hptr : self._hptr + self.plen] + return self._cache__packet_copy + + @property + def options(self) -> list: + """Read list of options""" + + if "_cache__options" not in self.__dict__: + self._cache__options: list = [] + optr = self._hptr + tcp.ps.HEADER_LEN + while optr < self._hptr + self.hlen: + if self._frame[optr] == tcp.ps.OPT_EOL: + self._cache__options.append(OptEol()) + break + if self._frame[optr] == tcp.ps.OPT_NOP: + self._cache__options.append(OptNop()) + optr += tcp.ps.OPT_NOP_LEN + continue + self._cache__options.append( + {tcp.ps.OPT_MSS: OptMss, tcp.ps.OPT_WSCALE: OptWscale, tcp.ps.OPT_SACKPERM: OptSackPerm, tcp.ps.OPT_TIMESTAMP: OptTimestamp}.get( + self._frame[optr], OptUnk + )(self._frame, optr) + ) + optr += self._frame[optr + 1] + + return self._cache__options + + @property + def mss(self) -> int: + """TCP option - Maximum Segment Size (2)""" + + if "_cache__mss" not in self.__dict__: + for option in self.options: + if option.kind == tcp.ps.OPT_MSS: + self._cache__mss = option.mss + break + else: + self._cache__mss = 536 + return self._cache__mss + + @property + def wscale(self) -> Optional[int]: + """TCP option - Window Scale (3)""" + + if "_cache__wscale" not in self.__dict__: + for option in self.options: + if option.kind == tcp.ps.OPT_WSCALE: + self._cache__wscale = 1 << option.wscale + break + else: + self._cache__wscale = None + return self._cache__wscale + + @property + def sackperm(self) -> Optional[bool]: + """TCP option - Sack Permit (4)""" + + if "_cache__sackperm" not in self.__dict__: + for option in self.options: + if option.kind == tcp.ps.OPT_SACKPERM: + self._cache__sackperm: Optional[bool] = True + break + else: + self._cache__sackperm = None + return self._cache__sackperm + + @property + def timestamp(self) -> Optional[tuple[int, int]]: + """TCP option - Timestamp (8)""" + + if "_cache__timestamp" not in self.__dict__: + for option in self.options: + if option.kind == tcp.ps.OPT_TIMESTAMP: + self._cache__timestamp: Optional[tuple[int, int]] = (option.tsval, option.tsecr) + break + else: + self._cache__timestamp = None + return self._cache__timestamp + + def _packet_integrity_check(self, pshdr_sum: int) -> str: + """Packet integrity check to be run on raw frame prior to parsing to make sure parsing is safe""" + + if not config.packet_integrity_check: + return "" + + if inet_cksum(self._frame, self._hptr, self._plen, pshdr_sum): + return "TCP integrity - wrong packet checksum" + + if not tcp.ps.HEADER_LEN <= self._plen <= len(self): + return "TCP integrity - wrong packet length (I)" + + hlen = (self._frame[self._hptr + 12] & 0b11110000) >> 2 + if not tcp.ps.HEADER_LEN <= hlen <= self._plen <= len(self): + return "TCP integrity - wrong packet length (II)" + + optr = self._hptr + tcp.ps.HEADER_LEN + while optr < self._hptr + hlen: + if self._frame[optr] == tcp.ps.OPT_EOL: + break + if self._frame[optr] == tcp.ps.OPT_NOP: + optr += 1 + if optr > self._hptr + hlen: + return "TCP integrity - wrong option length (I)" + continue + if optr + 1 > self._hptr + hlen: + return "TCP integrity - wrong option length (II)" + if self._frame[optr + 1] == 0: + return "TCP integrity - wrong option length (III)" + optr += self._frame[optr + 1] + if optr > self._hptr + hlen: + return "TCP integrity - wrong option length (IV)" + + return "" + + def _packet_sanity_check(self) -> str: + """Packet sanity check to be run on parsed packet to make sure frame's fields contain sane values""" + + if not config.packet_sanity_check: + return "" + + if self.sport == 0: + return "TCP sanity - 'sport' must be greater than 0" + + if self.dport == 0: + return "TCP sanity - 'dport' must be greater than 0" + + if self.flag_syn and self.flag_fin: + return "TCP sanity - 'flag_syn' and 'flag_fin' must not be set simultaneously" + + if self.flag_syn and self.flag_rst: + return "TCP sanity - 'flag_syn' and 'flag_rst' must not set simultaneously" + + if self.flag_fin and self.flag_rst: + return "TCP sanity - 'flag_fin' and 'flag_rst' must not be set simultaneously" + + if self.flag_fin and not self.flag_ack: + return "TCP sanity - 'flag_ack' must be set when 'flag_fin' is set" + + if self.ack and not self.flag_ack: + return "TCP sanity - 'flag_ack' must be set when 'ack' is not 0" + + if self.urg and not self.flag_urg: + return "TCP sanity - 'flag_urg' must be set when 'urg' is not 0" + + return "" + + +# +# TCP options +# + + +class OptEol(tcp.ps.OptEol): + """TCP option - End of Option List (0)""" + + def __init__(self): + self.kind = tcp.ps.OPT_EOL + + +class OptNop(tcp.ps.OptNop): + """TCP option - No Operation (1)""" + + def __init__(self): + self.kind = tcp.ps.OPT_NOP + + +class OptMss(tcp.ps.OptMss): + """TCP option - Maximum Segment Size (2)""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.kind = frame[optr + 0] + self.len = frame[optr + 1] + self.mss = struct.unpack_from("!H", frame, optr + 2)[0] + + +class OptWscale(tcp.ps.OptWscale): + """TCP option - Window Scale (3)""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.kind = frame[optr + 0] + self.len = frame[optr + 1] + self.wscale = frame[optr + 2] + + +class OptSackPerm(tcp.ps.OptSackPerm): + """TCP option - Sack Permit (4)""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.kind = frame[optr + 0] + self.len = frame[optr + 1] + + +class OptTimestamp(tcp.ps.OptTimestamp): + """TCP option - Timestamp (8)""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.kind = frame[optr + 0] + self.len = frame[optr + 1] + self.tsval = struct.unpack_from("!L", frame, optr + 2)[0] + self.tsecr = struct.unpack_from("!L", frame, optr + 6)[0] + + +class OptUnk(tcp.ps.OptUnk): + """TCP option not supported by this stack""" + + def __init__(self, frame: bytes, optr: int) -> None: + self.kind = frame[optr + 0] + self.len = frame[optr + 1] + self.data = frame[optr + 2 : optr + self.len] diff --git a/tcp_metadata.py b/tcp/metadata.py similarity index 75% rename from tcp_metadata.py rename to tcp/metadata.py index e4b09f0c..cfae6ca6 100755 --- a/tcp_metadata.py +++ b/tcp/metadata.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# tcp_metadata.py - module contains storage class for incoming TCP packet's metadata +# tcp/metadata.py - module contains storage class for incoming TCP packet's metadata # diff --git a/phrx_tcp.py b/tcp/phrx.py similarity index 78% rename from phrx_tcp.py rename to tcp/phrx.py index 377b88ed..36d72081 100755 --- a/phrx_tcp.py +++ b/tcp/phrx.py @@ -23,34 +23,27 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phrx_tcp.py - packet handler for inbound TCP packets +# tcp/phrx.py - packet handler for inbound TCP packets # -import fpp_tcp -import stack -from tcp_metadata import TcpMetadata +from typing import cast + +import misc.stack as stack +import tcp.fpp +from ip6.fpp import Parser as Ip6Parser +from misc.packet import PacketRx +from tcp.fpp import Parser as TcpParser +from tcp.metadata import TcpMetadata PACKET_LOSS = False -def _phrx_tcp(self, packet_rx): +def _phrx_tcp(self, packet_rx: PacketRx) -> None: """Handle inbound TCP packets""" - fpp_tcp.TcpPacket(packet_rx) + tcp.fpp.Parser(packet_rx) if packet_rx.parse_failed: if __debug__: @@ -60,6 +53,10 @@ def _phrx_tcp(self, packet_rx): if __debug__: self._logger.opt(ansi=True).info(f"{packet_rx.tracker} - {packet_rx.tcp}") + # Casting here does not matter if uses 6 or 4 + packet_rx.ip = cast(Ip6Parser, packet_rx.ip) + packet_rx.tcp = cast(TcpParser, packet_rx.tcp) + # Create TcpPacket object for further processing by TCP FSM packet = TcpMetadata( local_ip_address=packet_rx.ip.dst, diff --git a/phtx_tcp.py b/tcp/phtx.py similarity index 60% rename from phtx_tcp.py rename to tcp/phtx.py index 8d24b618..e93ab33b 100755 --- a/phtx_tcp.py +++ b/tcp/phtx.py @@ -23,59 +23,50 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phtx_tcp.py - packet handler for outbound TCP packets +# tcp/phtx.py - packet handler for outbound TCP packets # +from typing import Optional, Union + import config -from fpa_tcp import TcpOptMss, TcpOptNop, TcpOptWscale, TcpPacket -from ipv4_address import IPv4Address -from ipv6_address import IPv6Address +import tcp.fpa +from misc.ipv4_address import IPv4Address +from misc.ipv6_address import IPv6Address +from misc.tracker import Tracker PACKET_LOSS = False def _phtx_tcp( self, - ip_src, - ip_dst, - tcp_sport, - tcp_dport, - tcp_seq=0, - tcp_ack=0, - tcp_flag_ns=False, - tcp_flag_crw=False, - tcp_flag_ece=False, - tcp_flag_urg=False, - tcp_flag_ack=False, - tcp_flag_psh=False, - tcp_flag_rst=False, - tcp_flag_syn=False, - tcp_flag_fin=False, - tcp_mss=None, - tcp_win=0, - tcp_urp=0, - tcp_data=b"", - echo_tracker=None, -): + ip_src: Union[IPv6Address, IPv4Address], + ip_dst: Union[IPv6Address, IPv4Address], + tcp_sport: int, + tcp_dport: int, + tcp_seq: int = 0, + tcp_ack: int = 0, + tcp_flag_ns: bool = False, + tcp_flag_crw: bool = False, + tcp_flag_ece: bool = False, + tcp_flag_urg: bool = False, + tcp_flag_ack: bool = False, + tcp_flag_psh: bool = False, + tcp_flag_rst: bool = False, + tcp_flag_syn: bool = False, + tcp_flag_fin: bool = False, + tcp_mss: Optional[int] = None, + tcp_win: int = 0, + tcp_urp: int = 0, + tcp_data: bytes = b"", + echo_tracker: Optional[Tracker] = None, +) -> None: """Handle outbound TCP packets""" - assert type(ip_src) in {IPv4Address, IPv6Address} - assert type(ip_dst) in {IPv4Address, IPv6Address} + assert 0 < tcp_sport < 65536 + assert 0 < tcp_dport < 65536 # Check if IPv4 protocol support is enabled, if not then silently drop the IPv4 packet if not config.ip4_support and ip_dst.version == 4: @@ -85,14 +76,14 @@ def _phtx_tcp( if not config.ip6_support and ip_dst.version == 6: return - tcp_options = [] + tcp_options: list[Union[tcp.fpa.OptMss, tcp.fpa.OptNop, tcp.fpa.OptWscale]] = [] if tcp_mss: - tcp_options.append(TcpOptMss(tcp_mss)) - tcp_options.append(TcpOptNop()) - tcp_options.append(TcpOptWscale(0)) + tcp_options.append(tcp.fpa.OptMss(tcp_mss)) + tcp_options.append(tcp.fpa.OptNop()) + tcp_options.append(tcp.fpa.OptWscale(0)) - tcp_packet_tx = TcpPacket( + tcp_packet_tx = tcp.fpa.Assembler( sport=tcp_sport, dport=tcp_dport, seq=tcp_seq, @@ -126,7 +117,7 @@ def _phtx_tcp( return if ip_src.version == 6 and ip_dst.version == 6: - self._phtx_ip6(ip6_src=ip_src, ip6_dst=ip_dst, child_packet=tcp_packet_tx) + self._phtx_ip6(ip6_src=ip_src, ip6_dst=ip_dst, carried_packet=tcp_packet_tx) if ip_src.version == 4 and ip_dst.version == 4: - self._phtx_ip4(ip4_src=ip_src, ip4_dst=ip_dst, child_packet=tcp_packet_tx) + self._phtx_ip4(ip4_src=ip_src, ip4_dst=ip_dst, carried_packet=tcp_packet_tx) diff --git a/tcp/ps.py b/tcp/ps.py new file mode 100755 index 00000000..15ebe189 --- /dev/null +++ b/tcp/ps.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# tcp/ps.py - protocol support for TCP +# + + +# TCP packet header (RFC 793) + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Source Port | Destination Port | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Sequence Number | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Acknowledgment Number | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Hlen | Res |N|C|E|U|A|P|R|S|F| Window | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Checksum | Urgent Pointer | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# ~ Options ~ Padding ~ +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +HEADER_LEN = 20 + + +def __str__(self) -> str: + """Packet log string""" + + log = ( + f"TCP {self.sport} > {self.dport}, {'N' if self.flag_ns else ''}{'C' if self.flag_crw else ''}" + + f"{'E' if self.flag_ece else ''}{'U' if self.flag_urg else ''}{'A' if self.flag_ack else ''}" + + f"{'P' if self.flag_psh else ''}{'R' if self.flag_rst else ''}{'S' if self.flag_syn else ''}" + + f"{'F' if self.flag_fin else ''}, seq {self.seq}, ack {self.ack}, win {self.win}, dlen {len(self.data)}" + ) + + for option in self.options: + log += ", " + str(option) + + return log + + +# +# TCP options +# + + +# TCP option - End of Option List (0) + +OPT_EOL = 0 +OPT_EOL_LEN = 1 + + +class OptEol: + """TCP option - End of Option List (0)""" + + def __str__(self) -> str: + """Option log string""" + + return "eol" + + def __len__(self) -> int: + """Option length""" + + return OPT_EOL_LEN + + +# TCP option - No Operation (1) + +OPT_NOP = 1 +OPT_NOP_LEN = 1 + + +class OptNop: + """TCP option - No Operation (1)""" + + def __str__(self) -> str: + """Option log string""" + + return "nop" + + def __len__(self) -> int: + """Option length""" + + return OPT_NOP_LEN + + +# TCP option - Maximum Segment Size (2) + +OPT_MSS = 2 +OPT_MSS_LEN = 4 + + +class OptMss: + """TCP option - Maximum Segment Size (2)""" + + def __init__(self) -> None: + """Class constructor""" + + self.mss = -1 + + def __str__(self) -> str: + """Option log string""" + + return f"mss {self.mss}" + + def __len__(self) -> int: + """Option length""" + + return OPT_MSS_LEN + + +# TCP option - Window Scale (3) + +OPT_WSCALE = 3 +OPT_WSCALE_LEN = 3 + + +class OptWscale: + """TCP option - Window Scale (3)""" + + def __init__(self) -> None: + """Class constructor""" + + self.wscale = -1 + + def __str__(self) -> str: + """Option log string""" + + return f"wscale {self.wscale}" + + def __len__(self) -> int: + """Option length""" + + return OPT_WSCALE_LEN + + +# TCP option - Sack Permit (4) + +OPT_SACKPERM = 4 +OPT_SACKPERM_LEN = 2 + + +class OptSackPerm: + """TCP option - Sack Permit (4)""" + + def __str__(self) -> str: + """Option log string""" + + return "sack_perm" + + def __len__(self) -> int: + """Option length""" + + return OPT_SACKPERM_LEN + + +# TCP option - Timestamp + +OPT_TIMESTAMP = 8 +OPT_TIMESTAMP_LEN = 10 + + +class OptTimestamp: + """TCP option - Timestamp (8)""" + + def __init__(self) -> None: + """Class constructor""" + + self.tsval = -1 + self.tsecr = -1 + + def __str__(self) -> str: + """Option log string""" + + return f"ts {self.tsval}/{self.tsecr}" + + def __len__(self) -> int: + """Option length""" + + return OPT_TIMESTAMP_LEN + + +# TCP unknown option + + +class OptUnk: + """TCP option not supported by this stack""" + + def __init__(self) -> None: + """Class constructor""" + + self.kind = -1 + self.len = -1 + + def __str__(self) -> str: + """Option log string""" + + return f"unk-{self.kind}-{self.len}" diff --git a/tcp_session.py b/tcp/session.py similarity index 97% rename from tcp_session.py rename to tcp/session.py index 1f8ffc88..205e7fd3 100755 --- a/tcp_session.py +++ b/tcp/session.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# tcp_session_alt.py - module contains alternate version of class supporting TCP finite state machine, aimed to streamline and simplify original one +# tcp/session.py - module contains class supporting TCP finite state machine # @@ -47,7 +35,7 @@ import loguru import config -import stack +import misc.stack as stack PACKET_RETRANSMIT_TIMEOUT = 1000 # Retransmit data if ACK not received PACKET_RETRANSMIT_MAX_COUNT = 3 # If data is not acked, retransit it 5 times @@ -233,7 +221,7 @@ def _pick_random_local_port(self): """Pick random local port, making sure it is not already being used by any session bound to the same local IP""" used_ports = {int(_.split("/")[2]) for _ in stack.tcp_sessions if _.split("/")[1] in {"*", self.local_ip_address}} - while (port := random.randint(*config.TCP_EPHEMERAL_PORT_RANGE)) not in used_ports: + while (port := random.randint(*config.tcp_ephemeral_port_range)) not in used_ports: return port def _change_state(self, state): diff --git a/tcp_socket.py b/tcp/socket.py similarity index 83% rename from tcp_socket.py rename to tcp/socket.py index 0343459b..10a4f8bf 100755 --- a/tcp_socket.py +++ b/tcp/socket.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# tcp_socket.py - module contains class supporting TCP sockets +# tcp/socket.py - module contains class supporting TCP sockets # @@ -45,8 +33,8 @@ import loguru -import stack -from tcp_session import TcpSession +import misc.stack as stack +from tcp.session import TcpSession class TcpSocket: diff --git a/tests/test_misc_ip_helper.py b/tests/test_misc_ip_helper.py new file mode 100755 index 00000000..7437e1b5 --- /dev/null +++ b/tests/test_misc_ip_helper.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +from dataclasses import dataclass + +from testslide import TestCase + +from misc.ip_helper import inet_cksum, ip_pick_version + + +class TestMiscIpHelper(TestCase): + def test_inet_cksum(self): + @dataclass + class Sample: + data: bytes + init: int + result: int + + samples = [ + Sample(b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" * 80, 0, 0x2D2D), + Sample(b"\xFF" * 1500, 0, 0x0000), + Sample(b"\x00" * 1500, 0, 0xFFFF), + Sample(b"\xF7\x24\x09" * 100 + b"\x35\x67\x0F\x00" * 250, 0, 0xF1E5), + Sample(b"\x07" * 9999, 0, 0xBEC5), + Sample(b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" * 80, 0x03DF, 0x294E), + Sample(b"\xFF" * 1500, 0x0015, 0xFFEA), + Sample(b"\x00" * 1500, 0xF3FF, 0x0C00), + Sample(b"\xF7\x24\x09" * 100 + b"\x35\x67\x0F\x00" * 250, 0x7314, 0x7ED1), + Sample(b"\x07" * 9999, 0xA3DC, 0x1AE9), + ] + + for sample in samples: + result = inet_cksum(data=sample.data, dptr=0, dlen=len(sample.data), init=sample.init) + self.assertEqual(result, sample.result) + + def test_ip_pick_version(self): + + result = ip_pick_version("1:2:3:4:5:6:7:8") + from misc.ipv6_address import IPv6Address + + self.assertEqual(result, IPv6Address("1:2:3:4:5:6:7:8")) + + result = ip_pick_version("1.2.3.4") + from misc.ipv4_address import IPv4Address + + self.assertEqual(result, IPv4Address("1.2.3.4")) diff --git a/tests/test_misc_ph.py b/tests/test_misc_ph.py new file mode 100755 index 00000000..f394b7f2 --- /dev/null +++ b/tests/test_misc_ph.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +from testslide import StrictMock, TestCase + +from misc.ph import PacketHandler + + +class TestPacketHandler(TestCase): + def setUp(self): + super().setUp() + self.packet_handler = PacketHandler(None) + self.packet_handler._logger = StrictMock() + self.packet_handler._logger.debug = lambda _: None + self.packet_handler._logger.warning = lambda _: None + + def test__parse_stack_ip6_address_candidate(self): + from misc.ipv6_address import IPv6Address, IPv6Interface + + sample = [ + ("FE80::7/64", ""), # valid link loal address [pass] + ("FE80::77/64", ""), # valid link local address [pass] + ("FE80::7777/64", ""), # valid link local address [pass] + ("FE80::7777/64", ""), # valid duplicated address [fail] + ("FE80::9999/64", "FE80::1"), # valid link local address with default gateway [fail] + ("2007::1111/64", "DUPA"), # valid global address with malformed gateway [fail] + ("ZHOPA", ""), # malformed address [fail] + ("2099::99/64", "2222::99"), # valid global address with out of subnet gateway [fail] + ("2007::7/64", "FE80::1"), # valid global address with valid link local gateway [pass] + ("2009::9/64", "2009::1"), # valid global address with valid global gateway [pass] + ("2015::15/64", ""), # valid global address with no gateway [pass] + ] + expected = [ + IPv6Interface("fe80::7/64"), + IPv6Interface("fe80::77/64"), + IPv6Interface("fe80::7777/64"), + IPv6Interface("2007::7/64"), + IPv6Interface("2009::9/64"), + IPv6Interface("2015::15/64"), + ] + result = self.packet_handler._parse_stack_ip6_address_candidate(sample) + self.assertEqual(result, expected) + expected = [None, None, None, IPv6Address("fe80::1"), IPv6Address("2009::1"), None] + result = [ip6_address.gateway for ip6_address in result] + self.assertEqual(result, expected) + + def test__parse_stack_ip4_address_candidate(self): + from misc.ipv4_address import IPv4Address, IPv4Interface + + sample = [ + ("192.168.9.7/24", "192.168.9.1"), # valid address and valid gateway [pass] + ("192.168.9.77/24", "192.168.9.1"), # valid address and valid gateway [pass] + ("224.0.0.1/24", "192.168.9.1"), # invalid address [fail] + ("DUPA", "192.168.9.1"), # malformed address [fail] + ("192.168.9.99/24", "DUPA"), # malformed gateway [fail] + ("192.168.9.77/24", "192.168.9.1"), # duplicate address [fail] + ("192.168.9.170/24", "10.0.0.1"), # valid address but invalid gateway [fail] + ("192.168.9.171/24", "192.168.9.0"), # valid address but test invalid gateway [fail] + ("192.168.9.172/24", "192.168.9.172"), # valid address but invalid gateway [fail] + ("192.168.9.173/24", "192.168.9.255"), # valid address but invalid gateway [fail] + ("192.168.9.0/24", "192.168.9.1"), # invalid address [fail] + ("192.168.9.255/24", "192.168.9.1"), # invalid address [fail] + ("0.0.0.0/0", ""), # invalid address [fail] + ("192.168.9.102/24", ""), # valid address and no gateway [pass] + ("172.16.17.7/24", "172.16.17.1"), # valid address and valid gateway [pass] + ("10.10.10.7/24", "10.10.10.1"), # valid address and valid gateway [pass] + ] + expected = [ + IPv4Interface("192.168.9.7/24"), + IPv4Interface("192.168.9.77/24"), + IPv4Interface("192.168.9.102/24"), + IPv4Interface("172.16.17.7/24"), + IPv4Interface("10.10.10.7/24"), + ] + result = self.packet_handler._parse_stack_ip4_address_candidate(sample) + self.assertEqual(result, expected) + expected = [IPv4Address("192.168.9.1"), IPv4Address("192.168.9.1"), None, IPv4Address("172.16.17.1"), IPv4Address("10.10.10.1")] + result = [ip4_address.gateway for ip4_address in result] + self.assertEqual(result, expected) diff --git a/udp/__init__.py b/udp/__init__.py new file mode 100755 index 00000000..cfea1e74 --- /dev/null +++ b/udp/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# udp/__init__.py +# diff --git a/fpa_udp.py b/udp/fpa.py similarity index 52% rename from fpa_udp.py rename to udp/fpa.py index 59a3a5eb..2de9e120 100755 --- a/fpa_udp.py +++ b/udp/fpa.py @@ -23,67 +23,45 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# fpa_udp.py - Fast Packet Assembler support class for UDP protocol +# udp/fpa.py - Fast Packet Assembler support class for UDP protocol # import struct +from typing import Optional -from ip_helper import inet_cksum -from tracker import Tracker - -# UDP packet header (RFC 768) - -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Source port | Destination port | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -# | Packet length | Checksum | -# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - - -UDP_HEADER_LEN = 8 +import ip4.ps +import ip6.ps +import udp.ps +from misc.ip_helper import inet_cksum +from misc.tracker import Tracker -class UdpPacket: - """UDP packet support class""" +class Assembler: + """UDP packet assembler support class""" - protocol = "UDP" + ip4_proto = ip4.ps.PROTO_UDP + ip6_next = ip6.ps.NEXT_HEADER_UDP - def __init__(self, sport, dport, data=b"", echo_tracker=None): + def __init__(self, sport: int, dport: int, data: Optional[bytes] = None, echo_tracker: Optional[Tracker] = None) -> None: """Class constructor""" self.tracker = Tracker("TX", echo_tracker) - self.sport = sport self.dport = dport - self.data = data - self.plen = UDP_HEADER_LEN + len(self.data) - - def __str__(self): - """Packet log string""" + self.data = b"" if data is None else data + self.plen = udp.ps.HEADER_LEN + len(self.data) - return f"UDP {self.sport} > {self.dport}, len {self.plen}" - - def __len__(self): + def __len__(self) -> int: """Length of the packet""" return self.plen - def assemble_packet(self, frame, hptr, pshdr_sum): + from udp.ps import __str__ + + def assemble(self, frame: bytearray, hptr: int, pshdr_sum: int): """Assemble packet into the raw form""" struct.pack_into(f"! HH HH {len(self.data)}s", frame, hptr, self.sport, self.dport, self.plen, 0, self.data) diff --git a/udp/fpp.py b/udp/fpp.py new file mode 100755 index 00000000..857c4b84 --- /dev/null +++ b/udp/fpp.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# udp/fpp.py - Fast Packet Parser support class for UDP protocol +# + + +import struct +from typing import cast + +import config +import udp.ps +from ip6.fpp import Parser as Ip6Parser +from misc.ip_helper import inet_cksum +from misc.packet import PacketRx + + +class Parser: + """UDP packet parser class""" + + def __init__(self, packet_rx: PacketRx) -> None: + """Class constructor""" + + packet_rx.udp = self + + self._frame = packet_rx.frame + self._hptr = packet_rx.hptr + + # Casting here does not matter if uses 6 or 4 + packet_rx.ip = cast(Ip6Parser, packet_rx.ip) + self._plen = packet_rx.ip.dlen + + packet_rx.parse_failed = self._packet_integrity_check(packet_rx.ip.pshdr_sum) or self._packet_sanity_check() + + if not packet_rx.parse_failed: + packet_rx.hptr = self._hptr + udp.ps.HEADER_LEN + + def __len__(self) -> int: + """Number of bytes remaining in the frame""" + + return len(self._frame) - self._hptr + + from udp.ps import __str__ + + @property + def sport(self) -> int: + """Read 'Source port' field""" + + if "_cache__sport" not in self.__dict__: + self._cache__sport = struct.unpack_from("!H", self._frame, self._hptr + 0)[0] + return self._cache__sport + + @property + def dport(self) -> int: + """Read 'Destianation port' field""" + + if "_cache__dport" not in self.__dict__: + self._cache__dport = struct.unpack_from("!H", self._frame, self._hptr + 2)[0] + return self._cache__dport + + @property + def plen(self) -> int: + """Read 'Packet length' field""" + + if "_cache__plen" not in self.__dict__: + self._cache__plen = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] + return self._cache__plen + + @property + def cksum(self) -> int: + """Read 'Checksum' field""" + + if "_cache__cksum" not in self.__dict__: + self._cache__cksum = struct.unpack_from("!H", self._frame, self._hptr + 6)[0] + return self._cache__cksum + + @property + def data(self) -> bytes: + """Read the data packet carries""" + + if "_cache__data" not in self.__dict__: + self._cache__data = self._frame[self._hptr + udp.ps.HEADER_LEN : self._hptr + self.plen] + return self._cache__data + + @property + def dlen(self) -> int: + """Calculate data length""" + + return self.plen - udp.ps.HEADER_LEN + + @property + def packet(self) -> bytes: + """Read the whole packet""" + + if "_cache__packet" not in self.__dict__: + self._cache__packet = self._frame[self._hptr :] + return self._cache__packet + + def _packet_integrity_check(self, pshdr_sum: int) -> str: + """Packet integrity check to be run on raw frame prior to parsing to make sure parsing is safe""" + + if not config.packet_integrity_check: + return "" + + if inet_cksum(self._frame, self._hptr, self._plen, pshdr_sum): + return "UDP integrity - wrong packet checksum" + + if not udp.ps.HEADER_LEN <= self._plen <= len(self): + return "UDP integrity - wrong packet length (I)" + + plen = struct.unpack_from("!H", self._frame, self._hptr + 4)[0] + if not udp.ps.HEADER_LEN <= plen == self._plen <= len(self): + return "UDP integrity - wrong packet length (II)" + + return "" + + def _packet_sanity_check(self): + """Packet sanity check to be run on parsed packet to make sure frame's fields contain sane values""" + + if not config.packet_sanity_check: + return "" + + if self.sport == 0: + return "UDP sanity - 'udp_sport' must be greater than 0" + + if self.dport == 0: + return "UDP sanity - 'udp_dport' must be greater then 0" + + return "" diff --git a/udp_metadata.py b/udp/metadata.py similarity index 75% rename from udp_metadata.py rename to udp/metadata.py index c5de566a..8c2d2d12 100755 --- a/udp_metadata.py +++ b/udp/metadata.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# udp_metadata.py - module contains storage class for incoming UDP packet's metadata +# udp/metadata.py - module contains storage class for incoming UDP packet's metadata # diff --git a/phrx_udp.py b/udp/phrx.py similarity index 68% rename from phrx_udp.py rename to udp/phrx.py index b95d5084..6c8dd72c 100755 --- a/phrx_udp.py +++ b/udp/phrx.py @@ -23,39 +23,33 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phrx_udp.py - packet handler for inbound UDP packets +# udp/phrx.py - packet handler for inbound UDP packets # +from typing import cast + import loguru -import fpp_icmp4 -import fpp_icmp6 -import fpp_udp -import stack -from ipv4_address import IPv4Address -from ipv6_address import IPv6Address -from udp_metadata import UdpMetadata +import icmp4.ps +import icmp6.ps +import misc.stack as stack +import udp.fpp +from ip4.fpp import Parser as Ip4Parser +from ip6.fpp import Parser as Ip6Parser +from misc.ipv4_address import IPv4Address +from misc.ipv6_address import IPv6Address +from misc.packet import PacketRx +from udp.fpp import Parser as UdpParser +from udp.metadata import UdpMetadata -def _phrx_udp(self, packet_rx): +def _phrx_udp(self, packet_rx: PacketRx) -> None: """Handle inbound UDP packets""" - fpp_udp.UdpPacket(packet_rx) + udp.fpp.Parser(packet_rx) if packet_rx.parse_failed: if __debug__: @@ -65,6 +59,10 @@ def _phrx_udp(self, packet_rx): if __debug__: self._logger.opt(ansi=True).info(f"{packet_rx.tracker} - {packet_rx.udp}") + # Casting here does not matter if uses 6 or 4 + packet_rx.ip = cast(Ip6Parser, packet_rx.ip) + packet_rx.udp = cast(UdpParser, packet_rx.udp) + # Create UdpMetadata object and try to find matching UDP socket packet = UdpMetadata( local_ip_address=packet_rx.ip.dst, @@ -91,26 +89,28 @@ def _phrx_udp(self, packet_rx): ) return - # Respond with ICMPv4 Port Unreachable message if no matching socket has been found + # Respond with ICMP Port Unreachable message if no matching socket has been found if __debug__: self._logger.debug(f"Received UDP packet from {packet_rx.ip.src} to closed port {packet_rx.udp.dport}, sending ICMPv4 Port Unreachable") if packet_rx.ip.ver == 6: + packet_rx.ip = cast(Ip6Parser, packet_rx.ip) self._phtx_icmp6( - ip6_src=packet_rx.ip6.dst, - ip6_dst=packet_rx.ip6.src, - icmp6_type=fpp_icmp6.ICMP6_UNREACHABLE, - icmp6_code=fpp_icmp6.ICMP6_UNREACHABLE__PORT, + ip6_src=packet_rx.ip.dst, + ip6_dst=packet_rx.ip.src, + icmp6_type=icmp6.ps.UNREACHABLE, + icmp6_code=icmp6.ps.UNREACHABLE__PORT, icmp6_un_data=packet_rx.ip.packet_copy, echo_tracker=packet_rx.tracker, ) if packet_rx.ip.ver == 4: + packet_rx.ip = cast(Ip4Parser, packet_rx.ip) self._phtx_icmp4( ip4_src=packet_rx.ip.dst, ip4_dst=packet_rx.ip.src, - icmp4_type=fpp_icmp4.ICMP4_UNREACHABLE, - icmp4_code=fpp_icmp4.ICMP4_UNREACHABLE__PORT, + icmp4_type=icmp4.ps.UNREACHABLE, + icmp4_code=icmp4.ps.UNREACHABLE__PORT, icmp4_un_data=packet_rx.ip.packet_copy, echo_tracker=packet_rx.tracker, ) diff --git a/phtx_udp.py b/udp/phtx.py similarity index 59% rename from phtx_udp.py rename to udp/phtx.py index 53ab084c..5a8e55d1 100755 --- a/phtx_udp.py +++ b/udp/phtx.py @@ -23,35 +23,34 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# phtx_udp.py - protocol support for outbound UDP packets +# udp/phtx.py - protocol support for outbound UDP packets # +from typing import Union + import config -import fpa_udp -from ipv4_address import IPv4Address -from ipv6_address import IPv6Address +import udp.fpa +from misc.ipv4_address import IPv4Address +from misc.ipv6_address import IPv6Address +from misc.tracker import Tracker -def _phtx_udp(self, ip_src, ip_dst, udp_sport, udp_dport, udp_data=b"", echo_tracker=None): +def _phtx_udp( + self, + ip_src: Union[IPv6Address, IPv4Address], + ip_dst: Union[IPv6Address, IPv4Address], + udp_sport: int, + udp_dport: int, + udp_data: bytes = b"", + echo_tracker: Tracker = None, +) -> None: """Handle outbound UDP packets""" - assert type(ip_src) in {IPv4Address, IPv6Address} - assert type(ip_dst) in {IPv4Address, IPv6Address} + assert 0 < udp_sport < 65536 + assert 0 < udp_dport < 65536 # Check if IPv4 protocol support is enabled, if not then silently drop the IPv4 packet if not config.ip4_support and ip_dst.version == 4: @@ -61,13 +60,13 @@ def _phtx_udp(self, ip_src, ip_dst, udp_sport, udp_dport, udp_data=b"", echo_tra if not config.ip6_support and ip_dst.version == 6: return - udp_packet_tx = fpa_udp.UdpPacket(sport=udp_sport, dport=udp_dport, data=udp_data, echo_tracker=echo_tracker) + udp_packet_tx = udp.fpa.Assembler(sport=udp_sport, dport=udp_dport, data=udp_data, echo_tracker=echo_tracker) if __debug__: self._logger.opt(ansi=True).info(f"{udp_packet_tx.tracker} - {udp_packet_tx}") if ip_src.version == 6 and ip_dst.version == 6: - self._phtx_ip6(ip6_src=ip_src, ip6_dst=ip_dst, child_packet=udp_packet_tx) + self._phtx_ip6(ip6_src=ip_src, ip6_dst=ip_dst, carried_packet=udp_packet_tx) if ip_src.version == 4 and ip_dst.version == 4: - self._phtx_ip4(ip4_src=ip_src, ip4_dst=ip_dst, child_packet=udp_packet_tx) + self._phtx_ip4(ip4_src=ip_src, ip4_dst=ip_dst, carried_packet=udp_packet_tx) diff --git a/udp/ps.py b/udp/ps.py new file mode 100755 index 00000000..d3c89110 --- /dev/null +++ b/udp/ps.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +############################################################################ +# # +# PyTCP - Python TCP/IP stack # +# Copyright (C) 2020-2021 Sebastian Majewski # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see . # +# # +# Author's email: ccie18643@gmail.com # +# Github repository: https://github.com/ccie18643/PyTCP # +# # +############################################################################ + + +# +# udp/ps.py - protocol support for UDP +# + + +# UDP packet header (RFC 768) + +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Source port | Destination port | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +# | Packet length | Checksum | +# +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + + +HEADER_LEN = 8 + + +def __str__(self) -> str: + """Packet log string""" + + return f"UDP {self.sport} > {self.dport}, len {self.plen}" diff --git a/udp_socket.py b/udp/socket.py similarity index 76% rename from udp_socket.py rename to udp/socket.py index 5cbe7bd7..0e1c78e4 100755 --- a/udp_socket.py +++ b/udp/socket.py @@ -23,21 +23,9 @@ # # ############################################################################ -############################################################################################## -# # -# This program is a work in progress and it changes on daily basis due to new features # -# being implemented, changes being made to already implemented features, bug fixes, etc. # -# Therefore if the current version is not working as expected try to clone it again the # -# next day or shoot me an email describing the problem. Any input is appreciated. Also # -# keep in mind that some features may be implemented only partially (as needed for stack # -# operation) or they may be implemented in sub-optimal or not 100% RFC compliant way (due # -# to lack of time) or last but not least they may contain bug(s) that i didn't notice yet. # -# # -############################################################################################## - # -# udp_socket.py - module contains class supporting UDP sockets +# udp/socket.py - module contains class supporting UDP sockets # @@ -45,14 +33,12 @@ import loguru -import stack +import misc.stack as stack class UdpSocket: """Support for Socket operations""" - open_sockets: dict = {} - def __init__(self): """Class constructor"""