Basics: Packet sniffing/spoofing


So far we have watched packets that other programs created. Today we send our own packets and receive replies: first using ordinary sockets, where the operating system builds the headers for us, and then using raw sockets and Scapy, where we build every field by hand. That second capability is what makes sniffing (listening to all network traffic) and spoofing (sending "fake" packets) possible — if you can set any field to any value, you can claim to be any host on the network, and if you can read every frame that reaches your NIC, you can read traffic that was never addressed to you. Both are skills you'll need for Lab 1.

Slides from class

Files:

Additional information:

Agenda

These notes review the layers from the point of view of what each one adds to the packet, then work through the socket API in Python and C, raw sockets and packet filtering, and Scapy. Routing between LANs is still deferred; we assume here that packets somehow find their way across the network to their destination (standby for how this happens). Today we concentrate on what's in the packets.

Notes

Review: what each layer adds

We've described the layers by what they do. For writing packet code, the more useful framing is: what information does each layer contribute, and what does it wrap? Working down from the top, for a message leaving a host:

The receiver undoes this in reverse: the frame arrives at Layer 1, and each layer strips the header its counterpart added and passes the payload up, until the application gets back the bytes the sender's application handed down. A reply then runs the whole process again in the other direction.

TCP is connection-oriented, UDP is not

There are two choices at Layer 4, and the difference matters for everything you write today.

TCP (Transmission Control Protocol) establishes a connection between a sender and a receiver first, using a three-way handshake. Messages are broken into packets, each packet is given a sequence number, packets are reassembled in sequence order at the receiver, and anything missing is retransmitted. Delivery is, in the usual phrasing, "guaranteed."

UDP (User Datagram Protocol) establishes nothing. Messages are broken into packets and sent; each packet either arrives or doesn't, and nothing is resent. In exchange, UDP is faster, has smaller headers, and adds no setup delay, which is why it's the choice for streaming protocols.

A useful way to feel the difference: imagine writing a message across several sheets of paper and handing them to someone across the room. UDP is what happens if a couple of sheets get dropped on the way and a couple arrive in the wrong order — the receiver is left with a garbled message and no way to ask for the rest. TCP is what happens when the receiver acknowledges each sheet, the sender resends anything unacknowledged, and the receiver sorts them by page number before reading. The transport unit later in the term is largely about how TCP achieves that on top of a Layer 3 that promises nothing.

Ports identify applications

Your computer runs many applications at once. Without some way to distinguish them, all incoming data would land in the same place — your browser would receive your email and vice versa. Ports solve this: a port is just an integer from 0 to 65,535 (that is, 216 − 1), carried in the Layer 4 header.

The two sides use ports differently, which is worth being precise about:

The range is divided into three parts: 0–1023 are well-known ports, commonly reserved for system applications; 1024–49,151 are user or registered ports; and 49,152–65,535 are ephemeral ports, used for the randomly assigned client side. Some well-known ports worth memorizing:

20 and 21 — FTP
22 — SSH
23 — Telnet
53 — DNS
80 — HTTP
443 — encrypted HTTP (HTTPS)
587 (formerly 25) — email (SMTP)

The full list on a Unix machine lives in /etc/services.

One web request across two LANs

Consider two networks. A PC and a laptop share a LAN on subnet 192.168.1.0/24, connected by one switch. A web server and an email server share a different LAN on 10.0.0.0/8, connected by a different switch. The two LANs are joined by routers with the Internet in between. (The slash notation is CIDR, and we'll cover it properly soon; for now, read 192.168.1.0/24 as "the first 24 bits identify the network, the remaining 8 identify the host," giving 28 − 2 = 254 usable addresses, and 10.0.0.0/8 as leaving 24 host bits, or about 16.7 million addresses.)

The laptop at 192.168.1.3 requests a web page from the web server at 10.0.0.2. This is the client/server model that most of the Internet runs on: clients make requests, servers answer them. Here is the entire trip, with attention to which fields change and which don't.

The laptop builds the request. At Layer 7 it formats a message naming the web page it wants from the server. At Layer 4 it sets the destination port to 80 (well-known HTTP) and the source port to 1026 (random, unused, chosen by the OS). At Layer 3 it sets destination 10.0.0.2 and source 192.168.1.3. At Layer 2 it needs a destination MAC address — but 10.0.0.2 is not on its LAN, so the frame is addressed to the gateway router, whose IP address the laptop learned when it got its own address (statically or from DHCP) and whose MAC address it obtained through ARP. So the Layer 2 header reads: destination 00.E0.8F.60.A2.0A (Router1), source 00.0E.F7.55.B7.C8 (laptop).

The local switch forwards it. Switch0 knows only Layer 2. It reads the destination MAC, knows which physical port Router1 is plugged into, and sends the frame there.

Router1 rewrites Layer 2. The router confirms the destination MAC is its own, strips the Layer 2 header, and looks at Layer 3. Seeing destination 10.0.0.2, it determines that the next stop is Router2 (how it determines that is the routing unit we will cover soon). It then builds a new Layer 2 header: destination 00.D0.97.66.D4.3D (Router2), source 00.E0.8F.60.A2.0A (Router1). How did it know Router2's MAC address? ARP, again.

Router2 does the same. It checks that the destination MAC is its own MAC, strips the Layer 2 header, reads Layer 3 (destination 10.0.0.2, source 192.168.1.3), determines the next stop is the web server itself, and writes a new Layer 2 header: destination 00.0B.BE.B2.60.70 (web server), source 00.D0.97.66.D4.3D (Router2). The far switch reads that header and delivers the frame to the web server's port.

The web server decapsulates. It confirms the Layer 2 destination is its own MAC and strips that header. It checks Layer 3, stores the source IP (192.168.1.3) so it knows where to reply, and strips that header. It checks Layer 4, notes the destination port 80 means this is for the web application, stores the source port 1026 for the reply, and strips that header. What's left is the application data, which goes to the web app.

The reply reverses everything. Layer 7 is filled with the requested page's HTML. Layer 4 swaps the ports: source 80, destination 1026. Layer 3 swaps the addresses: source 10.0.0.2, destination 192.168.1.3. Layer 2 is addressed from the web server to Router2, and gets rewritten at each hop on the way back.

Now the question that makes this walkthrough worth doing. Can the web server learn the laptop's MAC address? No. Router1 stripped it off and overwrote it at the first hop. The IP addresses survived the entire journey end to end; the MAC addresses lasted exactly one link each. Layer 2 identity is local, Layer 3 identity is global, and any tool that reports a MAC address is telling you about the last hop, not about the original sender.

One simplification above: the local network is almost certainly using Network Address Translation (NAT). Addresses in 192.168.1.0/24 and 10.0.0.0/8 are non-routable — they are private ranges that cannot appear as-is on the public Internet — so the addresses actually used for routing between the two sites were not the ones shown here. We'll cover NAT and why it exists soon; the short answer is that we ran out of IPv4 addresses.

Sockets: the application's door to the network

A socket is an endpoint for two-way communication between programs over a network. A socket is defined by three things: an IP address, a port, and a protocol (TCP or UDP). It is the interface an application uses to hand data to the network or take data from it, and you've seen sockets before in CS 10 and CS 50.

The division of labor is the point. The application has data it wants to send, and passes that data to the OS through the socket API. The OS kernel generates the packet, adding the Transport, Network, and Link layer headers. The packet then goes out over the network via the NIC. The application never touches a header.

Concretely, the developer must decide and supply only a few things: which transport protocol to use (UDP for speed without delivery guarantees, TCP for guaranteed delivery at some cost), the destination IP address (Layer 3), and the destination port (Layer 4). The OS fills in everything else — a random unused source port, the source IP address, both MAC addresses, and the checksum for bit-error detection (covered soon).

One detail from the receiving side that will matter shortly: each NIC has a unique MAC address, and it drops frames not addressed to itself — unless it has been put into promiscuous or monitor mode. Hold that thought, we will come back to it.

Sending UDP in Python

Sending a packet through a socket is short work in Python (send_udp.py):

import socket

dest_addr = "127.0.0.1"
port = 9090
msg = b'Hello world!'

if __name__ == '__main__':
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(msg,(dest_addr,port))
    sock.close()

AF_INET asks for IPv4. SOCK_DGRAM asks for UDP; use SOCK_STREAM for TCP instead. The message must be bytes, hence the b'' prefix. sendto hands the data and the destination to the OS, which builds the Transport, Network, and Link headers and sends the result out the NIC.

Notice what is not in this program: any source port. We set a destination port because the client needs to know where the server is listening, but the OS picks our source port for us. That asymmetry is the client/server distinction from the ports section, in code.

To watch it work, listen with netcat in one terminal and run the program in another:

Terminal 1: $ python3 send_udp.py
Terminal 2: $ nc -luv 9090 → Hello world!

(nc flags: -l listen, -u UDP, -v verbose. netcat handles both TCP and UDP and can act as either client or server, which makes it a good scratch tool for testing anything you write.)

The same thing in C

Python's socket module is a wrapper over the C libraries, so the C version (send_udp.c) does the same work with the details exposed:

int main() {
    struct sockaddr_in dest_info;
    char *dest_addr = "127.0.0.1";
    int port = 9090;
    char *data = "Hello World (in C!)\n";

    //Create network socket
    int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

    //Provide needed data
    memset((char *) &dest_info,0,sizeof(dest_info));
    dest_info.sin_family = AF_INET;
    dest_info.sin_addr.s_addr = inet_addr(dest_addr);
    dest_info.sin_port = htons(port);

    //send packet
    sendto(sock, data, strlen(data), 0, (struct sockaddr *) &dest_info,
           sizeof(dest_info));
    close(sock);
}

Same three decisions (IPv4, UDP, destination), but here we fill in a sockaddr_in struct by hand: zero it out with memset, set the address family, convert the dotted-quad string to a binary address with inet_addr, and convert the port to network byte order with htons — more on that in the endianness section below. Compile and run with gcc send_udp.c -o send then ./send, again with netcat listening.

Receiving with a socket

Receiving is the mirror image, and it's worth knowing what happens beneath the API:

  1. The NIC examines incoming frames. If a frame is addressed to its MAC address, it copies the frame into a kernel ring buffer by DMA and interrupts the CPU to say a frame has arrived.
  2. The Layer 2 driver takes the frame from the ring buffer, strips the link-layer header, and passes it up if the MAC matched.
  3. Layer 3 checks for a match with the host's own IP address and passes it up to Layer 4 if it matches.
  4. Layer 4 looks at the destination port, fills a buffer, and delivers it to whichever socket is listening on that port.
  5. The application, which has been blocked waiting, is woken by the kernel and reads the data out of the buffer.

Note the constraint hiding in step 1: buffers have limited memory, so the CPU must handle frames quickly or they will be dropped. This is the same queueing-and-loss story from Day 1, now happening inside a single host.

In Python (receive_udp.py):

import socket

MAX_SIZE = 1500 #max message size in bytes
ip_addr = "0.0.0.0" #0.0.0.0 means bind to all interfaces
port = 9090  #listen on this port

if __name__ == '__main__':
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind((ip_addr,port))

    while (True):
        msg, (ip, port) = sock.recvfrom(MAX_SIZE)
        print(f"{ip}:{port} {msg.decode('utf-8')}")

The socket is created exactly as before, but now we bind it to an address and port. A machine may have several NICs; 0.0.0.0 means accept packets arriving on any of them. Note that here we do choose the port — this is the server side.

recvfrom blocks until a packet arrives and returns both the data and the sender's address information. (Use recvfrom for UDP and recv for TCP.) Run the receiver in one terminal and send with netcat in another (nc -u 127.0.0.1 9090), type hello, and the receiver prints something like:

127.0.0.1:64369 hello

That port is worth a second look: it is not 9090. It's the sender's source port, chosen at random by the sender's OS. Start two senders at once and you'll see two different ports:

127.0.0.1:53072 hello
127.0.0.1:52815 sup?

Which is precisely how the OS keeps concurrent instances of an application straight.

The C receiver (receive_udp.c) follows the same shape: create the socket with socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP), zero and fill a server struct (INADDR_ANY plays the role of Python's 0.0.0.0), bind, then loop on recvfrom, using inet_ntop to turn the client's binary address back into a printable string.

Can a receiver written in C accept packets from a sender written in Python? Yes — and neither cares whether the other machine runs macOS, Windows, or Linux. That's the whole point of standardized protocols: what crosses the network is a specified sequence of bytes, not a language-level or OS-level object.

Timeouts

A program often needs to wait for one of several possible events: either a reply arrives on the socket, or enough time passes that we should give up. (Waiting on several sockets at once is a related problem, solved with select() or threads.) Timeouts are used extensively in networking, for the obvious reason that on an unreliable network "no reply yet" and "no reply ever" look identical.

In Python, settimeout applies to all subsequent operations on that socket. The timer starts when a blocking call like recv or recvfrom begins; if a packet arrives first the timer stops, and if the interval elapses first the call is interrupted and a socket.timeout exception is raised (settimeout.py):

# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Set a timeout of 5 seconds for the socket operations
sock.settimeout(5)
serverAddressPort = ("127.0.0.1", 9090)
bytesToSend = str.encode("Hello UDP Server")
try:
    sock.sendto(bytesToSend, serverAddressPort)
    msgFromServer, address = sock.recvfrom(1024)
    print(f"Message from server: {msgFromServer.decode()}")
except socket.timeout:
    print("Socket operation timed out.")
except socket.error as e:
    print(f"Socket error: {e}")
finally:
    sock.close()

You'll want this in Lab 1: a ping that never gives up is a ping that hangs forever the first time a host is unreachable.

Endianness and network byte order

Endianness refers to the order in which a multi-byte value is stored in memory. Storing 0xDEADBEEF, with addresses increasing left to right:

Big endian:  DE AD BE EF
Little endian: EF BE AD DE

Big endian puts the most significant byte at the lowest address; little endian puts it at the highest. Little endian always looks backwards, and that is a reasonable reaction. Different architectures made different choices — x86 is little endian, while IBM z/Architecture mainframes and some others are big endian.

This is a problem for networking: two machines with different byte orders will misinterpret each other's multi-byte fields, such as a 16-bit port number. The solution is to agree on one order for anything sent over the network, called network order, which is big endian. Since Intel hosts are little endian, values must be converted between host order and network order on the way out and back. That's what htons ("host to network short") was doing in the C code above, and what ntohs does in the other direction. Python's socket module handles this for you, which is one of the things it means when we say Python hides complexity.

Sniffing: promiscuous mode, raw sockets, BPF, and PCAP

Recall that a NIC normally discards frames not addressed to its own MAC. In promiscuous mode it stops discarding them and passes every frame it sees up to the kernel. (For Wi-Fi the equivalent is monitor mode, which sniffs one channel at a time.) Combined with a raw socket — which delivers a copy of the whole frame, link-layer header included, straight to the application without the normal protocol-stack processing — this is how sniffers work.

Two practical notes. Most NICs support promiscuous mode, but not all do; recent Macs do not. And on the course VM you may need to allow it in the VM's network settings before an interface will hand you other hosts' frames.

In C, sniff_raw.c is the minimal version:

// Create the raw socket
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));

// Turn on the promiscuous mode.
mr.mr_type = PACKET_MR_PROMISC;
setsockopt(sock, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mr, sizeof(mr));

// Getting captured packets
while (1) {
    socklen_t saddr_len = sizeof(saddr);
    recvfrom(sock, buffer, PACKET_LEN, 0, &saddr, &saddr_len);
    if(data_size) printf("Got one packet\n");
}

ETH_P_ALL asks for every protocol (use ETH_P_IP for Layer 3 only), and the setsockopt call enables promiscuous mode. Be careful running this on a busy network: you will now be processing every frame in range, including every Wi-Fi beacon, and it is entirely possible to bog a machine down doing it.

Which is why we usually want to filter before frames are copied into userspace. The Berkeley Packet Filter (BPF) installs a filter at the link level so that only frames matching your criteria are passed up the stack. Filters are compiled and attached to a raw socket with setsockopt and SO_ATTACH_FILTER. BPF is powerful, somewhat complicated to write directly, and not portable between operating systems.

PCAP (packet capture) is the easier path. It was written originally for tcpdump, is available across platforms (libpcap on Linux, WinPcap and Npcap on Windows), and is the foundation under tcpdump, Wireshark, Scapy, Nmap, and Snort. It's written in C, and other languages generally wrap the C library. Using it (sniff.c) takes three steps: open a live capture session on an interface, compile and set a filter expression, and loop, handing each captured packet to a callback.

char filter_exp[] = "udp or icmp";

// Step 1: Open live pcap session on NIC
handle = pcap_open_live("en0", MAX_SIZE, 1, 1000, errbuf);

// Step 2: Compile filter_exp into BPF pseudo-code
pcap_compile(handle, &fp, filter_exp, 0, net);
if (pcap_setfilter(handle, &fp) != 0) { printf("set filter error"); return(1); }

// Step 3: Capture packets
pcap_loop(handle, -1, got_packet, NULL);

Note the third argument to pcap_open_live: that 1 is what turns on promiscuous mode. Find your interface name with ifconfig — these examples use the macOS name en0; on the course VM it's ens160. Note also that the raw-socket code above is Linux-specific (AF_PACKET), while PCAP works on both. PCAP compiles the filter expression to BPF for you, so you get link-level filtering without writing BPF by hand.

Even with PCAP, though, parsing captured packets in C is tedious. Why? Because the Layer 2, 3, and 4 headers are not of constant size — they have optional components — so you cannot simply index to a fixed byte offset to find a field. You have to read a header, work out how long it actually is, and only then know where the next one starts. This is the pain that Scapy removes.

Scapy: crafting and sniffing from Python

Scapy is a Python packet-manipulation library that does four things: builds packets of essentially any kind, sniffs packets off the wire, injects custom packets, and dissects captured packets into named fields. It works at Layers 2 through 7 and handles fiddly details like computing checksums. Install it with pip3 install scapy; it uses raw sockets, so programs that use it must run as root (sudo python3 prog.py).

Because every layer is an object with named fields, you can ask what's available at each level with ls():

$ sudo python3
>>> from scapy.all import *
>>> ls(IP)
version : BitField  (4 bits) = ('4')
ihl     : BitField  (4 bits) = ('None')
tos     : XByteField         = ('0')
len     : ShortField         = ('None')
id      : ShortField         = ('1')
flags   : FlagsField         = ('<Flag 0 ()>')
frag    : BitField  (13 bits)= ('0')
ttl     : ByteField          = ('64')
proto   : ByteEnumField      = ('0')
chksum  : XShortField        = ('None')
src     : SourceIPField      = ('None')
dst     : DestIPField        = ('None')
options : PacketListField    = ('[]')

ls(Ether) and ls(TCP) do the same for Layer 2 and TCP. Every one of those fields can be read or set. Where a value is shown as None, Scapy will compute something reasonable — lengths, checksums — and where it shows a value like ttl = 64, that's the default you'll get if you don't override it. This is the crucial property: you set only the fields you care about, and you may set any of them.

Sniffing with Scapy

Scapy sits on top of PCAP, so sniffing with a filter takes a few lines (sniff_scapy.py):

from scapy.all import *

def process_packet(pkt):
    global count
    print('-'*40)
    print("Packet number",count)
    pkt.show()
    print('-'*40)
    count += 1

if __name__ == '__main__':
    count = 0
    pkt = sniff(iface='en0', filter='icmp or udp', count=10, prn=process_packet)

iface picks the interface, filter takes the same expression syntax PCAP does (Scapy compiles it to BPF for you), count stops after ten packets, and prn names a callback invoked for each one. Run it with sudo, then ping 8.8.8.8 from another terminal, and pkt.show() prints the whole packet decoded layer by layer:

###[ Ethernet ]###
  dst       = 00:50:56:f5:9e:21
  src       = 00:0c:29:a8:d0:96
  type      = IPv4
###[ IP ]###
     ttl       = 64
     proto     = icmp
     chksum    = 0xf3c6
     src       = 192.168.159.129
     dst       = 8.8.8.8
###[ ICMP ]###
        type      = echo-request
        code      = 0
        id        = 0xb
        seq       = 0x1

Compare that to the byte-offset arithmetic the C version would need. (pkt.summary() gives a one-line version if the full dump is too much.)

Spoofing, and sniff-then-spoof

If you can set any field, you can lie about any field. Here is a spoofed UDP packet (spoof_udp.py):

#!/usr/bin/python3
from scapy.all import *

print("SENDING SPOOFED UDP PACKET.........")
ip = IP(src="1.2.3.4", dst="10.0.2.5") # IP Layer
udp = UDP(sport=8888, dport=9090)      # UDP Layer
data = "Hello UDP!\n"                  # Payload
pkt = ip/udp/data      # Construct the complete packet
pkt.show()
send(pkt,verbose=0)

Note the syntax: layers are objects and the / operator stacks them, so ip/udp/data reads as "UDP inside IP, carrying this payload" — encapsulation written as an expression. Scapy fills in the defaults and computes the checksums.

The important line is src="1.2.3.4". Nothing checked whether we own that address. A sniffer on the receiving end sees a packet that appears to have come from that address — a false flag operation if that is not our IP. We didn't create an Ether layer here, but we could have, and then the Layer 2 MAC addresses would be forged too. You'll do exactly that in Lab 1.

Now combine the two capabilities. sniff_spoof_icmp.py listens for ICMP echo requests and answers them — whether or not they were addressed to us:

from scapy.all import *

def spoof_pkt(pkt):
  if ICMP in pkt and pkt[ICMP].type == 8:
     #listen for ICMP request packets (type 8)
     print("Original Packet.........")
     print("Source IP : ", pkt[IP].src)
     print("Destination IP :", pkt[IP].dst)

     #spoof a reply, even if the request wasn't for us
     #must reverse source and destination on reply!
     ip = IP(src=pkt[IP].dst, dst=pkt[IP].src, ihl=pkt[IP].ihl)
     icmp = ICMP(type=0, id=pkt[ICMP].id, seq=pkt[ICMP].seq)
     data = pkt[Raw].load
     newpkt = ip/icmp/data

     send(newpkt,verbose=0)

pkt = sniff(filter='icmp and src host 10.0.2.15',prn=spoof_pkt)

The pattern is: receive a packet, extract what you need, build a new one with source and destination reversed, keep the id and sequence number so the reply matches the request, and send. The effect is a phantom host: pings sent to an address where nothing exists get answered anyway. Everything the requester uses to decide "that host is alive" — a reply with the right addresses, the right ICMP type, and the right sequence number — is a field an attacker controls.

Scapy vs. C

Scapy's advantages are all about productivity: it's easy to write, sets reasonable defaults, computes derived values like checksums, lets you focus only on the fields you care about, and makes every layer an object you can stack. Its disadvantage is that it is slow.

C's advantages are the mirror image: it runs 50–100 times faster and gives you complete control over how a packet is crafted. Its disadvantage is that it's tricky and tedious to write, and you have to get the byte offsets right.

Sometimes speed genuinely matters — particularly when you're racing to reply before the legitimate host does, as in the sniff-and-spoof example. Sending 1000 UDP packets takes Scapy about 9.4 seconds and C about 0.25 seconds, roughly 37× faster. A hybrid approach gets both: use Scapy to craft the packets and save them to a file, then read them in C (adjusting fields as needed) and send them from there.

Background: classful addressing

Some context for the addressing material coming up. From 1981 to 1993, IP addresses were handed out under a classful addressing scheme, in fixed blocks, partly to simplify routing. With 32 bits total:

The problem is visible in the arithmetic: an organization got 224, 216, or 28 addresses and nothing in between. An organization needing 300 addresses had to take a Class B and leave 65,000 unused. Addresses were consumed far faster than the actual number of connected hosts warranted, which is what led to CIDR, and eventually to NAT and IPv6 (topics we will cover soon).

Exercises

To do before/around the next class:

Terms

Not a comprehensive reference — the textbook has much more — but these are the terms from Day 3 that will keep coming back.

client/server model
The dominant Internet interaction pattern: clients initiate requests, servers listen on well-known ports and respond.
TCP (Transmission Control Protocol)
Connection-oriented Layer 4 protocol. Establishes a connection with a three-way handshake, numbers packets, reassembles them in order, and retransmits what's missing.
UDP (User Datagram Protocol)
Connectionless Layer 4 protocol. No handshake, no sequencing, no retransmission; faster, with smaller headers. Used for streaming and for protocols like DHCP.
port
An integer 0–65,535 in the Layer 4 header identifying an application (server side) or an application instance (client side). Well-known: 0–1023; registered: 1024–49,151; ephemeral: 49,152–65,535. See /etc/services.
socket
An endpoint for two-way network communication, defined by an IP address, a port, and a protocol. The interface through which an application hands data to the OS, which builds the headers.
bind
Associating a socket with a local address and port so it can receive. 0.0.0.0 (or INADDR_ANY in C) means accept on any interface.
AF_INET / SOCK_DGRAM / SOCK_STREAM
Socket options selecting IPv4, UDP, and TCP respectively.
netcat (nc)
A command-line tool that sends or receives TCP and UDP traffic, useful as the other end when testing your own network code. -l listen, -u UDP, -v verbose.
ring buffer
The kernel memory region a NIC copies arriving frames into by DMA before the link-layer driver processes them. Finite, so slow processing means dropped frames.
timeout
A limit on how long a socket operation will block before raising an exception, set in Python with settimeout. Necessary because a missing reply and a slow reply are indistinguishable.
endianness
The order in which the bytes of a multi-byte value are stored. Big endian puts the most significant byte at the lowest address; little endian the reverse. x86 is little endian.
network order / host order
Network order is the agreed byte order for data on the network, which is big endian. Host order is whatever the local architecture uses. Convert with htons and ntohs.
promiscuous mode
A NIC setting that passes up every frame it receives rather than discarding those not addressed to its own MAC. The Wi-Fi equivalent, monitor mode, captures one channel.
raw socket
A socket that delivers a complete frame, link-layer header included, directly to the application, bypassing normal stack processing. Requires root privileges.
BPF (Berkeley Packet Filter)
A filter installed at the link level so only matching frames are copied up to userspace, saving processing. Powerful, awkward to write directly, not portable across OSes.
PCAP
The cross-platform packet capture library (libpcap, Npcap) written for tcpdump and now underlying Wireshark, Scapy, Nmap, and Snort. Compiles filter expressions to BPF for you.
Scapy
A Python library for crafting, sending, sniffing, and dissecting packets at Layers 2–7. Sets sensible defaults, computes checksums, and stacks layers with the / operator. Needs root; much slower than C.
spoofing
Setting header fields — source IP address, source MAC address — to values other than your own, so a packet appears to originate elsewhere. Nothing in the protocols prevents it.
NAT (Network Address Translation)
Rewriting addresses so hosts on a private, non-routable range (e.g. 192.168.0.0/16, 10.0.0.0/8) can share a public address. A response to IPv4 address exhaustion.
classful addressing
The 1981–1993 scheme allocating addresses in fixed Class A/B/C blocks of 224, 216, or 28 addresses, identifiable by their leading bits. Wasteful, and replaced by CIDR.