365 Days of Code - Day 038

Project Status

ProjectLanguageStatusDue DateLatest Update
Personal WebsiteHugoOngoingNoneThe site is live. There are some TODOs. Need to work on categorization, tagging, and layout improvements.
Laravel From ScratchLaravel (PHP)In-Progress2026-03-31Episode 8
PRMLaravel (PHP)In-Progress2026-03-31Working alongside other Laravel projects.
Client Website (J.L.)Laravel (PHP)In-Progress2026-03-31Working alongside other Laravel projects.
Project EulerCOngoingNoneWorking on P25. BigInt (AI gen) was a waste of time, need to rewrite
Practice JavaJavaPausedNoneInstalled, need to find a good project.
Practice PythonPythonPausedNoneInstalled, need to find a good project.
Learn GoGoPausedNoneInstalled, work on LDAP Injector from ippsec.
Learn RustRustHaven’t StartedNoneInstalled, will try network protocols after finishing in C and Zig.
Learn ElixirElixirHaven’t StartedNoneInstalled, need a good tutorial project.
Learn HaskellHaskellHaven’t StartedNoneInstalled, need a good tutorial project.
Learn ZigZigHaven’t StartedNoneInstalled, will try network protocols after finishing in C.
Linux+N/AIn-Progress2026-03-31Reading Chapter 4.
Cyber Quest 2026N/AIn-Progress2026-02-28Finished quiz 1 with 75%. Need to work on ARP poisoning and timestamp adjustments in WireShark.
Operating SystemsN/AIn-Progress2026-03-31Reading Chapter 4: Abstraction
Grey-Hat HackingVariousIn-Progress2026-03-31Reading Chapter 8: Threat Hunting Lab
PHP Time TrackerPHPBeta FinishedNoneWorking on a basic level. Could use a couple more updates to make it fully functional.
HTTP Status Code ReaderCComplete2026-02-18Complete. Could potentially upgrade for more advanced functions or follow redirects.
ZSH Configurationbash/zshCompleteNoneSort of an ongoing process, but complete for now. Works good.
Network ProtocolsCIn-ProgressNoneIPv4 header, ICMP header and payload complete. Working on sockets.

The POSIX Standard, Transitive Dependencies, and Networking Code

While getting started on the socket configuration to send the newly constructed ICMP packet, I ran into a question about the header files in C and the things these files include. This led me down the rabbit hole of reading the POSIX standard (opens in a new tab).

This arose due to my usage of AF_INET macro. Especially on programs where I am learning or practicing, I like to include the specific functionality I’m using from each header file in a comment.

c
#include <stdio.h>
#include <stdint.h>      // fixed type sizes (uint8_t, etc)
#include <assert.h>      // turn assumptions into assertions
#include <stddef.h>      // offsetof()
#include <arpa/inet.h>   // htons(), ntohs(), inet_pton(), AF_INET
#include <string.h>      // memcpy()
1
2
3
4
5
6
#include <stdio.h>
#include <stdint.h>      // fixed type sizes (uint8_t, etc)
#include <assert.h>      // turn assumptions into assertions
#include <stddef.h>      // offsetof()
#include <arpa/inet.h>   // htons(), ntohs(), inet_pton(), AF_INET
#include <string.h>      // memcpy()

Prior to today, I used the header arpa/inet.h to get access to the AF_INET macro for defining the IPv4 addresses. However, I was reading up on sys/socket.h, which I’ll be including today, and I noticed that it also can include AF_INET. This immediately caught my eye, because I would only expect a single implementation of functions or macros from core libraries. As it turns out, AF_INET is part of a transitive dependency involving several libraries. On Ubuntu Linux 24.04 at least, this dependency chain is as follows:

  • arpa/inet.h includes
  • netinet/in.h includes
  • sys/socket.h includes
  • bits/socket.h defines AF_INET
  • AF_INET is defined by PF_INET
  • PF_INET is equal to 2

AF stands for Address Family and PF stands for Protocol Family.

Per Beej’s Guide to Network Programming (opens in a new tab) section 5.2:

This PF_INET thing is a close relative of the AF_INET that you can use when initializing the sin_family field in your struct sockaddr_in. In fact, they’re so closely related that they actually have the same value, and many programmers will call socket() and pass AF_INET as the first argument instead of PF_INET. Now, get some milk and cookies, because it’s time for a story. Once upon a time, a long time ago, it was thought that maybe an address family (what the “AF” in “AF_INET” stands for) might support several protocols that were referred to by their protocol family (what the “PF” in “PF_INET” stands for). That didn’t happen. And they all lived happily ever after, The End. So the most correct thing to do is to use AF_INET in your struct sockaddr_in and PF_INET in your call to socket().

Note: The above advice is outdated, as the Linux man pages (opens in a new tab) states:

The manifest constants used under 4.x BSD for protocol families are PFUNIX, PF_INET, and so on, while AF_UNIX, AF_INET, and so on are used for address families. However, already the BSD man page promises: “The protocol family generally is the same as the address family”, and subsequent standards use AF* everywhere.

Researching the discussions above in Beej’s book led me to Vint Cerf (opens in a new tab), one of the fathers of the internet. I always knew of Tim Berners-Lee (opens in a new tab) but Sir Timothy wouldn’t have been able to create the world wide web without the prerequisite work performed by Mr. Cerf. Vint Cerf wrote some of the original RFCs back in the 60s, such as RFC 20 - ASCII format for Network Interchange (opens in a new tab). He has contributed to much more than this, and is still active in the community as of February 2026.

But I digress, back to the AF_INET/PF_INET macro.

PF_INET gets referenced in a network handler, such as the one that can be seen in the Linux kernel source code. af_inet.c is named in the source comments as the PF_INET protocol family socket handler. Here is one excerpt of its usage.

  • net/ipv4/af_inet.c
c
const struct proto_ops inet_stream_ops = {
  .family = PF_INET,
  // ...
}
1
2
3
4
const struct proto_ops inet_stream_ops = {
  .family = PF_INET,
  // ...
}

In this example code, we know the .family is being set to 2, meaning this ops table is for sockets in the IPv4 family.

So, that is all very interesting. But, if sys/socket.h is transitively included by arpa/inet.h, why would we need to add a specific include statement for sys/socket.h? Technically, our program should work without it. But, our program becomes less portable. To write a strictly POSIX-conforming application, you must explicitly include the specific header file that the POSIX standard mandates for each function or macro used. The POSIX standard states that the socket functions (opens in a new tab) are owned by sys/socket.h. Therefore, this file should be explicitly included as a best practice when working with sockets.

The goal of this project isn’t necessarily portability, as the network code I’m writing is purely for educational purposes, and not intended to be used in a production application. However, going down rabbit holes such as the POSIX standard happens to be very educational. I’ll include the socket header as a defined best practice.

Raw Sockets

I completed the socket programming today, and successfully sent an echo request and received an echo reply using my custom ip/icmp packet. It was all pretty satisfying. This required the use of raw sockets, which is a root level permission on Linux. This is due to the fact that typically the kernel would provide the headers and help prevent packet spoofing.

c
  // Building a socket, transmit the packet

  // Why is root required to run this code now? Short answer, because we are using raw sockets
  // In standard network programs, the kernel abstracts away the IP layer, and generates the headers
  // This prevents spoofing or other unsafe operations
  // In this instance, we are specifically telling the kernel that we will provide all header information
  // Standard users cannot do this, and root privileges must be used to perform these operations

  // Create a raw socket explicitly bound to the ICMP protocol (so we can receive replies)
  int raw_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
  if (raw_socket < 0)
  {
    perror("Socket creation failed. Are you running as root?");
    return -1;
  }

  // Manually instruct the kernel that we are providing our own IPv4 Header (IP_HDRINCL)
  int hdr_incl = 1;
  if (setsockopt(raw_socket, IPPROTO_IP, IP_HDRINCL, &hdr_incl, sizeof(hdr_incl)) < 0)
  {
    perror("Failed to set IP_HDRINCL");
    close(raw_socket);
    return -1;
  }

  printf("Phase 4: Raw socket successfully created with IP_HDRINCL (fd: %d).\n", raw_socket);

  // Construct the targeting structure for the kernel's routing table
  struct sockaddr_in dest_info = {0};

  dest_info.sin_family = AF_INET;
  dest_info.sin_port = htons(0); // ICMP does not use Layer 4 ports
  inet_pton(AF_INET, "127.0.0.1", &dest_info.sin_addr.s_addr);

  // Test 21: Validate the kernel routing structure
  assert(dest_info.sin_family == AF_INET);
  assert(dest_info.sin_port == 0);
  assert(ntohl(dest_info.sin_addr.s_addr) == 0x7F000001);
  printf("Test 21 Passed: Kernel routing structure (sockaddr_in) populated.\n");

  // Inject the packet_buffer onto the wire
  ssize_t bytes_sent = sendto(raw_socket, packet_buffer, ntohs(ip->total_length), 0,
                (struct sockaddr *)&dest_info, sizeof(dest_info));

  if (bytes_sent < 0)
  {
    perror("Failed to send packet");
    close(raw_socket);
    return -1;
  }

  // Test 22: Validate transmission size
  assert(bytes_sent == ntohs(ip->total_length));
  printf("Test 22 Passed: Successfully injected %zd bytes onto the wire.\n", bytes_sent);
  printf("Use `sudo tcpdump -vv -i lo -n icmp` to see the results.\n");

  uint8_t recv_buffer[1024] = {0};
  struct sockaddr_in sender_info;
  socklen_t sender_len = sizeof(sender_info);

  struct ipv4_header *recv_ip = NULL;
  struct icmp_header *recv_icmp = NULL;

  printf("Listening for Echo Reply...\n");

  while (1)
  {
    ssize_t bytes_received = recvfrom(raw_socket, recv_buffer, sizeof(recv_buffer), 0,
                      (struct sockaddr *)&sender_info, &sender_len);
    if (bytes_received < 0)
      continue; // Skip socket read errors

    recv_ip = (struct ipv4_header *)recv_buffer;

    // Ensure it's ICMP and from 127.0.0.1
    if (recv_ip->protocol != IP_PROTO_ICMP || ntohl(recv_ip->src_addr) != 0x7F000001)
    {
      continue;
    }

    uint8_t recv_ihl = recv_ip->version_ihl & 0x0F;
    size_t ip_header_bytes = recv_ihl * 4;
    recv_icmp = (struct icmp_header *)(recv_buffer + ip_header_bytes);

    // If it is our Echo Request reflecting back, ignore it. We only want the Reply.
    if (recv_icmp->type == ECHO_REPLY)
    {
      printf("Test 25 Passed: Ignored our own request and caught the true Echo Reply!\n");
      break; // We found the target packet, exit the loop!
    }
  }

  // Map the Echo-specific fields immediately after the baseline ICMP header
  struct icmp_echo *recv_echo = (struct icmp_echo *)((uint8_t *)recv_icmp + sizeof(struct icmp_header));

  // Test 26: Validate the Identifier and Sequence
  assert(ntohs(recv_echo->identifier) == 0x1234);
  assert(ntohs(recv_echo->sequence) == 0x0001);
  printf("Test 26 Passed: Echo Reply Identifier and Sequence match our Request.\n");

  // Map a pointer to the start of the returned payload
  uint8_t *recv_payload = (uint8_t *)recv_echo + sizeof(struct icmp_echo);

  // Test 27: Validate the payload content
  // memcmp returns 0 if the memory blocks are identical
  assert(memcmp(recv_payload, "HELLO", 5) == 0);
  printf("Test 27 Passed: Payload successfully extracted and verified as 'HELLO'.\n");
  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
  // Building a socket, transmit the packet

  // Why is root required to run this code now? Short answer, because we are using raw sockets
  // In standard network programs, the kernel abstracts away the IP layer, and generates the headers
  // This prevents spoofing or other unsafe operations
  // In this instance, we are specifically telling the kernel that we will provide all header information
  // Standard users cannot do this, and root privileges must be used to perform these operations

  // Create a raw socket explicitly bound to the ICMP protocol (so we can receive replies)
  int raw_socket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
  if (raw_socket < 0)
  {
    perror("Socket creation failed. Are you running as root?");
    return -1;
  }

  // Manually instruct the kernel that we are providing our own IPv4 Header (IP_HDRINCL)
  int hdr_incl = 1;
  if (setsockopt(raw_socket, IPPROTO_IP, IP_HDRINCL, &hdr_incl, sizeof(hdr_incl)) < 0)
  {
    perror("Failed to set IP_HDRINCL");
    close(raw_socket);
    return -1;
  }

  printf("Phase 4: Raw socket successfully created with IP_HDRINCL (fd: %d).\n", raw_socket);

  // Construct the targeting structure for the kernel's routing table
  struct sockaddr_in dest_info = {0};

  dest_info.sin_family = AF_INET;
  dest_info.sin_port = htons(0); // ICMP does not use Layer 4 ports
  inet_pton(AF_INET, "127.0.0.1", &dest_info.sin_addr.s_addr);

  // Test 21: Validate the kernel routing structure
  assert(dest_info.sin_family == AF_INET);
  assert(dest_info.sin_port == 0);
  assert(ntohl(dest_info.sin_addr.s_addr) == 0x7F000001);
  printf("Test 21 Passed: Kernel routing structure (sockaddr_in) populated.\n");

  // Inject the packet_buffer onto the wire
  ssize_t bytes_sent = sendto(raw_socket, packet_buffer, ntohs(ip->total_length), 0,
                (struct sockaddr *)&dest_info, sizeof(dest_info));

  if (bytes_sent < 0)
  {
    perror("Failed to send packet");
    close(raw_socket);
    return -1;
  }

  // Test 22: Validate transmission size
  assert(bytes_sent == ntohs(ip->total_length));
  printf("Test 22 Passed: Successfully injected %zd bytes onto the wire.\n", bytes_sent);
  printf("Use `sudo tcpdump -vv -i lo -n icmp` to see the results.\n");

  uint8_t recv_buffer[1024] = {0};
  struct sockaddr_in sender_info;
  socklen_t sender_len = sizeof(sender_info);

  struct ipv4_header *recv_ip = NULL;
  struct icmp_header *recv_icmp = NULL;

  printf("Listening for Echo Reply...\n");

  while (1)
  {
    ssize_t bytes_received = recvfrom(raw_socket, recv_buffer, sizeof(recv_buffer), 0,
                      (struct sockaddr *)&sender_info, &sender_len);
    if (bytes_received < 0)
      continue; // Skip socket read errors

    recv_ip = (struct ipv4_header *)recv_buffer;

    // Ensure it's ICMP and from 127.0.0.1
    if (recv_ip->protocol != IP_PROTO_ICMP || ntohl(recv_ip->src_addr) != 0x7F000001)
    {
      continue;
    }

    uint8_t recv_ihl = recv_ip->version_ihl & 0x0F;
    size_t ip_header_bytes = recv_ihl * 4;
    recv_icmp = (struct icmp_header *)(recv_buffer + ip_header_bytes);

    // If it is our Echo Request reflecting back, ignore it. We only want the Reply.
    if (recv_icmp->type == ECHO_REPLY)
    {
      printf("Test 25 Passed: Ignored our own request and caught the true Echo Reply!\n");
      break; // We found the target packet, exit the loop!
    }
  }

  // Map the Echo-specific fields immediately after the baseline ICMP header
  struct icmp_echo *recv_echo = (struct icmp_echo *)((uint8_t *)recv_icmp + sizeof(struct icmp_header));

  // Test 26: Validate the Identifier and Sequence
  assert(ntohs(recv_echo->identifier) == 0x1234);
  assert(ntohs(recv_echo->sequence) == 0x0001);
  printf("Test 26 Passed: Echo Reply Identifier and Sequence match our Request.\n");

  // Map a pointer to the start of the returned payload
  uint8_t *recv_payload = (uint8_t *)recv_echo + sizeof(struct icmp_echo);

  // Test 27: Validate the payload content
  // memcmp returns 0 if the memory blocks are identical
  assert(memcmp(recv_payload, "HELLO", 5) == 0);
  printf("Test 27 Passed: Payload successfully extracted and verified as 'HELLO'.\n");

I’m now working on a second version that is a bit more dynamic, and a little less loaded with useless comments and asserts. The complete source code is available at Github (opens in a new tab).