Code Network Stack In CPP
Full code: https://github.com/auspham/learn/tree/master/cpp/networking
Header declaration
TCP (Transmission Control Protocol) Header
#include <cstdint>
#include <netinet/in.h>
struct __attribute__((packed)) TCPHeader {
/**
* The source port of the application
*/
uint16_t source_port;
/**
* End port of the application. i.e 80 for http
*/
uint16_t destination_port;
/**
* TCP breaks data to small chunk, index of the current chunk.
* To send to receiver for them to assemble out of order
*/
uint32_t sequence_number;
/**
* Next sequence number expected to be received
* Typically = sequence_number + length_of_data_sent
*/
uint32_t acknowledgment_number;
/**
* 4 bit identify length of the header
* 4 bit reserved
*/
uint8_t header_length_and_reserved;
/**
* 8 1 bit flags
*/
uint8_t flags;
/**
* 16 bit for flow control, how many bytes sender allow to transmit without
* receiving an acknowledgement
*/
uint16_t window_size;
/**
* 16 bit checksum for error detection
*/
uint16_t checksum;
/**
* only use when URG flag is set
*/
uint16_t urgent_pointer;
};
In here, we need to combine the header_length and reserved bit (4 bits each) into one since if we declare:
uint8_t header_length: 4, reserved: 4;
This one, since CPP is little-endian, it will revert the bit and makes reserved first header_length later — which could be problematic to us.
The use of packed struct here is because Memory padding we want our header to be 20 bytes exact no padding
IP (Internet Protocol)v4 header
#include <cstdint>
struct __attribute__((packed)) IPV4Header {
// Version 4 and header length is total is 20 bytes uint8 = 1 byte, uint16 = 2 byte, uint32 = 4 byte
// Since header length is 20, the value in here should use is 5, since they take header_length x 4 byte = actual size of byte in header
uint8_t version_and_header_length;
uint8_t type_of_service;
uint16_t total_length;
uint16_t identification;
uint8_t ip_flag;
uint8_t flagment_offset;
uint8_t time_to_live;
uint8_t protocol;
uint16_t header_checksum;
uint32_t source_address;
uint32_t destination_address;
};
Same thing here, we combine version and header_length together since little-endian, they're 4 bits each.
EthernetII Header
#include <cstdint>
struct __attribute__((packed)) EthernetHeader {
uint8_t destination_mac[6];
uint8_t source_mac[6];
uint16_t type;
};
Header Visualisation
Loading...
When packet receive from switch to switch, it strip each layer out and process.
In real world, it would look like this
sequenceDiagram
participant Net as Network (Physical Wire)
participant NIC as NIC (Data Link Layer)
participant OS as OS Kernel (Network/Transport)
participant App as Application Code (User Space)
Note over Net, App: --- INBOUND: DECAPSULATION ---
Net->>NIC: 1. Arrival: Binary Bits (Electrical/Light)
Note right of NIC: Packet: [Eth][IP][TCP][HTTP][Data]
NIC->>NIC: 2. Remove Ethernet II Header
Note right of NIC: Strips MAC Addresses & FCS
NIC->>OS: 3. Pass IPv4 Datagram
Note right of OS: Packet: [IP][TCP][HTTP][Data]
OS->>OS: 4. Remove IPv4 Header
Note right of OS: Strips Source/Dest IP
OS->>OS: 5. Remove TCP Header
Note right of OS: Strips Ports & Reassembles Stream
OS->>App: 6. Pass HTTP Payload (via Socket)
Note right of App: Packet: [HTTP][Data]
App->>App: 7. Remove HTTP Header
Note right of App: Parses Method (GET/POST) & Headers
App->>App: 8. Logic Processing (Application Code)
Note over Net, App: --- OUTBOUND: ENCAPSULATION ---
App->>App: 9. Create Response Data
App->>App: 10. Add HTTP Header
App->>OS: 11. Write to Socket
OS->>OS: 12. Add TCP Header (Ports/Seq #)
OS->>OS: 13. Add IPv4 Header (IP Addresses)
OS->>NIC: 14. Send Ethernet Frame
NIC->>NIC: 15. Add Ethernet II Header (MACs/CRC)
NIC->>Net: 16. Departure: Binary Bits
Packet forming
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <netinet/in.h>
#include <stdint.h>
#include <string>
#include "models/packet.hpp"
#include <iostream>
#include <arpa/inet.h>
Packet::Packet(std::string content) {
Packet::content = content;
EthernetHeader ethernet_header = Packet::defaultEthernetHeader();
IPV4Header ipv4_header = Packet::defaultIPV4Header();
TCPHeader tcp_header = Packet::defaultTCPHeader();
uint8_t content_length = content.length();
ipv4_header.total_length = htons(content_length + sizeof(tcp_header) + sizeof(ipv4_header));
std::cout << "Ethernet Header: " << sizeof(ethernet_header) << std::endl;
std::cout << "IPV4 Header: " << sizeof(ipv4_header) << std::endl;
std::cout << "TCP Header: " << sizeof(tcp_header) << std::endl;
std::cout << "Data length: " << +content_length << " : " << content << std::endl;
std::cout << "Total length: " << ntohs(ipv4_header.total_length) << std::endl;
void* data = (void*) malloc(ipv4_header.total_length + sizeof(ethernet_header));
uint8_t* ptr = (uint8_t*) data;
memcpy(ptr, &ethernet_header, sizeof(ethernet_header));
ptr += sizeof(ethernet_header);
memcpy(ptr, &ipv4_header, sizeof(ipv4_header));
ptr += sizeof(ipv4_header);
memcpy(ptr, &tcp_header, sizeof(tcp_header));
ptr += sizeof(tcp_header);
memcpy(ptr, content.c_str(), content_length);
ptr += content_length;
Packet::data = data;
this->ipv4 = ipv4_header;
this->tcp = tcp_header;
this->ether = ethernet_header;
}
EthernetHeader Packet::defaultEthernetHeader() {
EthernetHeader header = {
.destination_mac = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
.source_mac = {0x11, 0x11, 0x11, 0x11, 0x11, 0x11},
.type = htons(0x0800) // 0x0800 is the EtherType for IPv4
};
return header;
}
TCPHeader Packet::defaultTCPHeader() {
TCPHeader header = {
.source_port = htons(80),
.destination_port = htons(80),
.sequence_number = htonl(0),
.acknowledgment_number = htonl(0),
.header_length_and_reserved = 0x50, // each unit length = 4 bytes, 4x5 = 20 bytes
.flags = 0x02, // SYNC flag
.window_size = htons(65535),
.checksum = 0,
.urgent_pointer = 0
};
return header;
}
IPV4Header Packet::defaultIPV4Header() {
IPV4Header header = {
// Need to combine this, otherwise little-endian will reorder the bytes. If we form 1 byte already, the compiler will just use it and drop in 1 byte
.version_and_header_length = 0x45,
.type_of_service = 0,
// We will calculate this later, this includes TCP and HTTP header
.total_length = 0,
.identification = htons(0),
.ip_flag = 0x0,
.flagment_offset = 0,
.time_to_live = 0x40,
.protocol = IPPROTO_TCP,
.header_checksum = 0,
.source_address = inet_addr("127.0.0.1"),
.destination_address = inet_addr("127.0.0.1")
};
return header;
}
Packet::~Packet() {}
One thing to note here is for IP (Internet Protocol), the total length does not include EthernetII header
Sending to interface
#include <cstdio>
#include <iostream>
#include <sys/socket.h>
#include "models/packet.hpp"
#include <linux/if_packet.h>
#include <net/if.h>
#include <cstring>
#include <unistd.h>
#include <stdio.h>
int main() {
Packet packet ("Content");
int sock = socket(AF_PACKET, SOCK_RAW, packet.ether.type);
if (sock < 0) {
std::cerr << "Error creating socket" << std::endl;
return 1;
}
sockaddr_ll addr = {
.sll_family = AF_PACKET,
.sll_protocol = packet.ether.type,
.sll_ifindex = (int) if_nametoindex("lo"),
// ssl_hatype and ssl_pkttype are set on received packets, we're sending
.sll_hatype = 0,
.sll_pkttype = 0,
.sll_halen = sizeof(packet.ether.destination_mac),
};
std::memcpy(addr.sll_addr, packet.ether.destination_mac, addr.sll_halen);
ssize_t packet_length = ntohs(packet.ipv4.total_length) + sizeof(packet.ether);
ssize_t res = sendto(sock, packet.data, packet_length, 0, (sockaddr*)&addr, sizeof(addr));
std::cout << "Result: " << res << std::endl;
close(sock);
}
❯ make
Ethernet Header: 14
IPV4 Header: 20
TCP Header: 20
Data length: 7 : Content
Total length: 47
Result: 61
Result
0000 00 00 00 00 00 00 11 11 11 11 11 11 08 00 45 00 ..............E.
0010 00 2f 00 00 00 00 40 06 00 00 7f 00 00 01 7f 00 ./....@.........
0020 00 01 00 50 00 50 00 00 00 00 00 00 00 00 50 02 ...P.P........P.
0030 ff ff 00 00 00 00 43 6f 6e 74 65 6e 74 ......Content
Frame 1: 61 bytes on wire (488 bits), 61 bytes captured (488 bits) on interface lo, id 0
Ethernet II, Src: 11:11:11:11:11:11 (11:11:11:11:11:11), Dst: 00:00:00_00:00:00 (00:00:00:00:00:00)
Destination: 00:00:00_00:00:00 (00:00:00:00:00:00)
Address: 00:00:00_00:00:00 (00:00:00:00:00:00)
.... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
.... ...0 .... .... .... .... = IG bit: Individual address (unicast)
Source: 11:11:11:11:11:11 (11:11:11:11:11:11)
[Expert Info (Warning/Protocol): Source MAC must not be a group address: IEEE 802.3-2002, Section 3.2.3(b)]
[Source MAC must not be a group address: IEEE 802.3-2002, Section 3.2.3(b)]
[Severity level: Warning]
[Group: Protocol]
Address: 11:11:11:11:11:11 (11:11:11:11:11:11)
.... ..0. .... .... .... .... = LG bit: Globally unique address (factory default)
.... ...1 .... .... .... .... = IG bit: Group address (multicast/broadcast)
Type: IPv4 (0x0800)
Internet Protocol Version 4, Src: 127.0.0.1, Dst: 127.0.0.1
0100 .... = Version: 4
.... 0101 = Header Length: 20 bytes (5)
Differentiated Services Field: 0x00 (DSCP: CS0, ECN: Not-ECT)
Total Length: 47
Identification: 0x0000 (0)
0. .... = Flags: 0x0
0... .... = Reserved bit: Not set
.0.. .... = Don't fragment: Not set
..0. .... = More fragments: Not set
...0 0000 0000 0000 = Fragment Offset: 0
Time to Live: 64
Protocol: TCP (6)
Header Checksum: 0x0000 [validation disabled]
[Header checksum status: Unverified]
Source Address: 127.0.0.1
Destination Address: 127.0.0.1
Transmission Control Protocol, Src Port: 80, Dst Port: 80, Seq: 0, Len: 7
Source Port: 80
Destination Port: 80
[Stream index: 0]
[Conversation completeness: Incomplete (9)]
[TCP Segment Len: 7]
Sequence Number: 0 (relative sequence number)
Sequence Number (raw): 0
[Next Sequence Number: 8 (relative sequence number)]
Acknowledgment Number: 0
Acknowledgment number (raw): 0
0101 .... = Header Length: 20 bytes (5)
Flags: 0x002 (SYN)
Window: 65535
[Calculated window size: 65535]
Checksum: 0x0000 [unverified]
[Checksum Status: Unverified]
Urgent Pointer: 0
[Timestamps]
[SEQ/ACK analysis]
TCP payload (7 bytes)
TCP segment data (7 bytes)