Advanced

Raw Sockets: Forging Custom TCP Packets in C

Learn how to bypass the kernel's network stack using raw sockets in C. A byte-level walkthrough of IP and TCP header construction, checksum calculation, and SYN packet crafting.

When you call send() on a regular TCP socket, the kernel handles the entire protocol stack: port assignment, sequence numbering, checksum computation, the three-way handshake. For application code, that’s appropriate. For network tooling β€” port scanners, packet injectors, custom protocol implementations β€” it’s a constraint. You need to control what goes on the wire, not what the kernel decides to put there.

Raw sockets let you construct IP and TCP headers manually and hand the finished packet directly to the network interface. This is how tools like Nmap’s SYN scan (-sS) work, and how TCP session hijacking and custom protocol testing tools are implemented.

This article walks through the full process: opening a raw socket with the right flags, constructing IP and TCP headers correctly, computing the TCP checksum with its pseudo-header, and sending a forged SYN packet. The code compiles and runs on Linux with CAP_NET_RAW capability or root.


Raw Socket Types on Linux

Linux offers two relevant socket configurations for packet crafting:

/* Option A: kernel provides IP header, you provide TCP + payload */
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
/* Option B: you provide everything β€” IP + TCP + payload */
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

These behave differently in an important way:

  • IPPROTO_TCP raw socket: you receive inbound TCP packets matching your process (useful for reading SYN-ACK responses). The kernel still fills in the IP header on outbound sends unless IP_HDRINCL is set.
  • IPPROTO_RAW: outbound only β€” you cannot receive packets on this socket type. IP_HDRINCL is implicitly set. Use this when you only need to inject.

For a complete SYN scanner that reads responses, use IPPROTO_TCP with IP_HDRINCL. For pure injection (SYN flood, custom protocol probing), IPPROTO_RAW is simpler. The code below uses IPPROTO_RAW for the injection half; capturing responses requires a separate receive socket or libpcap.

To enable full IP header control with IPPROTO_TCP:

int one = 1;
setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one));

With IP_HDRINCL set, sendto() treats your buffer as a complete packet starting from the IP header. If any field is wrong β€” incorrect length, bad checksum, misaligned header β€” the packet will be silently dropped by the first router.

Privileges

Raw sockets require CAP_NET_RAW. Running as root satisfies this automatically. On systems where you want to avoid full root, set the capability explicitly:

Terminal window
sudo setcap cap_net_raw+ep ./packet_crafter

Packet Memory Layout

A packet buffer is a flat byte array with the IP header at offset 0, TCP header immediately after, and optional payload beyond that:

Offset Size Field
────── ──── ─────────────────────────────────────
0x00 20 B IP header (struct iphdr)
0x14 20 B TCP header (struct tcphdr)
0x28 var Payload (empty for a SYN probe)

Linux provides both structs in <netinet/ip.h> and <netinet/tcp.h>. You allocate a zeroed buffer and cast pointers to these structs β€” the compiler does the field offset arithmetic.


Byte Order: Host vs Network

x86 is little-endian; network byte order is big-endian. Every multi-byte integer in the packet headers must be converted before writing:

FunctionConverts
htons(x)16-bit host β†’ network (ports, lengths, IP ID)
htonl(x)32-bit host β†’ network (addresses, sequence numbers)
ntohs(x)16-bit network β†’ host
ntohl(x)32-bit network β†’ host

A port 80 in memory on an x86 machine without conversion would be stored as 0x50 0x00, which a network device reads as port 20480. Every port, address, sequence number, and length field in both headers needs the appropriate conversion.


The TCP Checksum

The TCP checksum is a 16-bit one’s complement sum over a pseudo-header concatenated with the TCP header and payload. The pseudo-header is not transmitted β€” it exists only for checksum computation, providing coverage over source/destination IPs and protocol to detect misrouted packets.

Pseudo-header structure (12 bytes):

struct pseudo_header {
uint32_t src_addr;
uint32_t dst_addr;
uint8_t zero; /* always 0 */
uint8_t protocol; /* 6 for TCP */
uint16_t tcp_length; /* TCP header + payload length, in network byte order */
};

Checksum algorithm β€” one’s complement sum of all 16-bit words, folded to 16 bits, then bitwise NOT:

static uint16_t checksum(const void *data, int len) {
const uint16_t *buf = data;
uint32_t sum = 0;
while (len > 1) {
sum += *buf++;
len -= 2;
}
if (len == 1)
sum += *(const uint8_t *)buf;
/* fold 32-bit accumulator to 16 bits */
while (sum >> 16)
sum = (sum & 0xFFFF) + (sum >> 16);
return (uint16_t)~sum;
}

The fold while (sum >> 16) handles carries properly β€” a single sum = (sum >> 16) + (sum & 0xFFFF) can itself produce a carry if the inputs are large, so the loop is more correct than a single addition.

The IP header has its own independent checksum, computed over just the IP header bytes (with iph->check zeroed first).


Complete SYN Packet Crafter

syn_craft.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
struct pseudo_header {
uint32_t src_addr;
uint32_t dst_addr;
uint8_t zero;
uint8_t protocol;
uint16_t tcp_length;
};
static uint16_t checksum(const void *data, int len)
{
const uint16_t *buf = data;
uint32_t sum = 0;
while (len > 1) { sum += *buf++; len -= 2; }
if (len == 1) sum += *(const uint8_t *)buf;
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);
return (uint16_t)~sum;
}
int main(void)
{
const char *src_ip = "192.168.1.100"; /* spoofed source β€” see note below */
const char *dst_ip = "192.168.1.200";
uint16_t dst_port = 80;
uint16_t src_port = 12345;
/* Raw socket β€” outbound only; use IPPROTO_TCP for bidirectional */
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
if (sock < 0) {
perror("socket (requires CAP_NET_RAW or root)");
return 1;
}
/* Buffer sized for IP + TCP headers, no payload */
char packet[sizeof(struct iphdr) + sizeof(struct tcphdr)];
memset(packet, 0, sizeof(packet));
struct iphdr *iph = (struct iphdr *)packet;
struct tcphdr *tcph = (struct tcphdr *)(packet + sizeof(struct iphdr));
/* ── IP header ──────────────────────────────────────────────────────── */
iph->version = 4;
iph->ihl = 5; /* 5 Γ— 4 = 20 bytes, no options */
iph->tos = 0;
iph->tot_len = htons(sizeof(packet)); /* total length in network byte order */
iph->id = htons(0xd431); /* 16-bit field β€” use htons, not htonl */
iph->frag_off = 0;
iph->ttl = 64;
iph->protocol = IPPROTO_TCP;
iph->check = 0; /* zeroed before checksum */
iph->saddr = inet_addr(src_ip);
iph->daddr = inet_addr(dst_ip);
iph->check = checksum(iph, sizeof(struct iphdr));
/* ── TCP header ─────────────────────────────────────────────────────── */
srand((unsigned)time(NULL));
tcph->source = htons(src_port);
tcph->dest = htons(dst_port);
tcph->seq = htonl((uint32_t)rand()); /* randomised β€” predictable ISN is exploitable */
tcph->ack_seq = 0;
tcph->doff = 5; /* 20-byte header, no TCP options */
tcph->syn = 1;
tcph->window = htons(65535);
tcph->check = 0;
tcph->urg_ptr = 0;
/* ── TCP checksum via pseudo-header ─────────────────────────────────── */
struct pseudo_header psh = {
.src_addr = inet_addr(src_ip),
.dst_addr = inet_addr(dst_ip),
.zero = 0,
.protocol = IPPROTO_TCP,
.tcp_length = htons(sizeof(struct tcphdr)),
};
char pseudo_buf[sizeof(struct pseudo_header) + sizeof(struct tcphdr)];
memcpy(pseudo_buf, &psh, sizeof(psh));
memcpy(pseudo_buf + sizeof(psh), tcph, sizeof(struct tcphdr));
tcph->check = checksum(pseudo_buf, sizeof(pseudo_buf));
/* ── Transmit ────────────────────────────────────────────────────────── */
struct sockaddr_in dst = {
.sin_family = AF_INET,
.sin_addr.s_addr = inet_addr(dst_ip),
};
if (sendto(sock, packet, sizeof(packet), 0,
(struct sockaddr *)&dst, sizeof(dst)) < 0) {
perror("sendto");
close(sock);
return 1;
}
printf("SYN sent: %s:%u β†’ %s:%u (seq=%u)\n",
src_ip, src_port, dst_ip, dst_port, ntohl(tcph->seq));
close(sock);
return 0;
}

Compile and run:

Terminal window
gcc -O2 -Wall -o syn_craft syn_craft.c
sudo ./syn_craft
# or without sudo if CAP_NET_RAW is set on the binary

Verify the packet on the wire with tcpdump on the same machine (different terminal):

Terminal window
sudo tcpdump -i any -nn 'tcp[tcpflags] & tcp-syn != 0 and host 192.168.1.200'

Notes on IP Spoofing

The source IP in the code above is arbitrary β€” raw sockets let you write any value into iph->saddr. There are two practical constraints:

BCP38 filtering: Most ISPs and enterprise networks implement BCP38 (ingress filtering), which drops packets whose source IP is not within the network’s own prefix range. Spoofed packets sent across the public internet rarely reach their destination β€” they’re dropped at the first upstream router. Spoofing is more practical on local networks or lab environments.

Response routing: A spoofed source IP means replies go to whoever owns that IP, not to you. For a SYN scan where you want to read the SYN-ACK, use your real source IP or a loopback address, and capture responses with a receive socket or libpcap. For pure injection (SYN flood testing in a controlled environment), spoofing is straightforward.


Reading Responses with a Receive Socket

IPPROTO_RAW is outbound only. To receive inbound packets (e.g., SYN-ACK responses to your forged SYN), open a second socket:

int recv_sock = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);

This receives all inbound TCP packets delivered to your host. You’ll need to filter by source IP and port in your read loop, since you receive everything, not just responses to your SYN:

char recv_buf[65535];
struct sockaddr_in sender;
socklen_t sender_len = sizeof(sender);
ssize_t len = recvfrom(recv_sock, recv_buf, sizeof(recv_buf), 0,
(struct sockaddr *)&sender, &sender_len);
struct iphdr *r_iph = (struct iphdr *)recv_buf;
struct tcphdr *r_tcph = (struct tcphdr *)(recv_buf + r_iph->ihl * 4);
if (r_tcph->syn && r_tcph->ack) {
printf("SYN-ACK from %s:%u β€” port %u is open\n",
inet_ntoa(sender.sin_addr),
ntohs(r_tcph->source),
ntohs(r_tcph->source));
}

One complication: the kernel will also send a RST in response to the SYN-ACK (because from its perspective, it never sent the original SYN). To suppress this, use iptables to drop outgoing RSTs to the target before running your scanner:

Terminal window
sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -d 192.168.1.200 -j DROP

Remove the rule afterwards:

Terminal window
sudo iptables -D OUTPUT -p tcp --tcp-flags RST RST -d 192.168.1.200 -j DROP

What to Use in Practice

For production tooling, libpcap handles capture far more cleanly than raw receive sockets β€” it gives you filtered access to the wire at the capture layer with a mature API. libnet handles packet construction with a higher-level interface than hand-rolling structs. Python’s scapy library combines both into a scripting environment that’s faster to iterate with than C for exploratory work.

The C implementation above is the right starting point for understanding what those libraries do internally, or for embedding packet crafting into a larger C application where you need tight control over performance or dependencies.

V

Author

Varkin Academy

Tags

#cyber security #networking #c #low-level #raw sockets #TCP #packet crafting