The data-link layer is the lowest software layer in the TCP/IP stack — it sits between the IP layer and the actual hardware. Understanding it helps you debug network issues, understand IP fragmentation, and write efficient socket applications. The most important concept here for socket programmers is the MTU (Maximum Transmission Unit).
The data-link layer is the bottom layer of the TCP/IP software stack. It consists of two parts:
- The device driver in the operating system kernel
- The network interface hardware — the network card (NIC)
Together, these connect the software world (IP datagrams) to the physical world (actual signals on a cable or radio waves in Wi-Fi).
The data-link layer is concerned with one specific task: transferring data across a single physical link. Notice the word “single” — IP handles routing across many hops; the data-link layer only deals with one hop at a time.
When the IP layer passes a datagram down to the data-link layer, the data-link layer encapsulates it inside a frame. A frame is the unit of data that is actually transmitted over the physical link.
6 bytes
6 bytes
2 bytes
(IP Datagram)
46–1500 bytes
4 bytes
What the data-link layer does with frames:
| Function | Description | All data-links? |
|---|---|---|
| Framing | Wraps the IP datagram with a header (dest address, size) and trailer (CRC) | Yes |
| Transmission | Sends frames across the physical link | Yes |
| Acknowledgement | Some protocols acknowledge receipt of each frame | No — not Ethernet |
| Error detection | CRC checksum detects corrupted frames | Most |
| Retransmission | Some protocols retransmit lost/corrupted frames | No — Ethernet leaves this to TCP |
| Flow control | Some protocols rate-control the sender to not overwhelm the receiver | No — Ethernet leaves this to TCP |
| Fragmentation | Some data-link layers split large packets into multiple frames and reassemble at the receiver | No |
The MTU (Maximum Transmission Unit) is the largest frame payload the data-link layer can transmit in a single frame. In Ethernet, the MTU for the IP payload is 1500 bytes.
Different data-link technologies have different MTUs:
| Technology | MTU (bytes) | Notes |
|---|---|---|
| Ethernet (standard) | 1500 | Most common LAN/internet MTU |
| Wi-Fi (802.11) | 1500 | Same as Ethernet at IP level |
| Loopback (lo) | 65536 | Local machine only, very large |
| Ethernet (Jumbo frames) | 9000 | Data center networks, reduces overhead |
| PPP (dial-up) | 576 | Old modem connections |
Why does MTU matter for socket programming? When an IP datagram is larger than the MTU, IP must fragment it — split it into multiple smaller datagrams that each fit in one frame. These fragments are reassembled at the destination. Fragmentation adds overhead and can cause performance issues.
IP header + 1480 bytes data
= 1500 bytes total ✓
IP header + 1500 bytes data
= 1520 bytes total ✓
The book mentions netstat -i as the way to list interfaces and their MTUs. Modern Linux also provides ip link for this. Here are both:
# Classic command: list network interfaces with MTU
netstat -i
# Sample output:
# Kernel Interface table
# Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
# eth0 1500 12345 0 0 0 8765 0 0 0 BMRU
# lo 65536 1234 0 0 0 1234 0 0 0 LRU
# Modern command: same info, more readable
ip link show
# Specific interface MTU:
cat /sys/class/net/eth0/mtu
# Or using ip:
ip link show eth0 | grep mtu
Important flags in netstat -i output:
- B — Broadcast supported
- M — Multicast supported
- R — Running (interface is active)
- U — Up (interface is enabled)
- L — Loopback
Example 1: Read the MTU of a network interface programmatically
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h> /* struct ifreq */
#include <netinet/in.h>
#include <unistd.h>
int get_interface_mtu(const char *ifname)
{
int sockfd;
struct ifreq ifr;
/* Need a socket to make the ioctl call — any AF_INET socket will do */
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
return -1;
}
memset(&ifr, 0, sizeof(ifr));
strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
/* SIOCGIFMTU = get interface MTU */
if (ioctl(sockfd, SIOCGIFMTU, &ifr) < 0) {
perror("ioctl SIOCGIFMTU");
close(sockfd);
return -1;
}
close(sockfd);
return ifr.ifr_mtu;
}
int main(void)
{
const char *ifaces[] = {"lo", "eth0", "wlan0", NULL};
for (int i = 0; ifaces[i] != NULL; i++) {
int mtu = get_interface_mtu(ifaces[i]);
if (mtu > 0)
printf("Interface %-10s MTU = %d bytes\n", ifaces[i], mtu);
}
return 0;
}
/* Compile: gcc -o get_mtu get_mtu.c
Run: ./get_mtu
Output:
Interface lo MTU = 65536 bytes
Interface eth0 MTU = 1500 bytes */
Example 2: List all interfaces with their MTU and MAC address
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <ifaddrs.h>
#include <arpa/inet.h>
#include <netpacket/packet.h>
#include <unistd.h>
int main(void)
{
struct ifaddrs *ifap, *ifa;
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
getifaddrs(&ifap);
printf("%-12s %-6s %-18s %s\n", "Interface", "MTU", "MAC Address", "IP Address");
printf("%-12s %-6s %-18s %s\n", "---------", "---", "-----------", "----------");
for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
if (ifa->ifa_addr == NULL) continue;
/* Get MTU via ioctl */
struct ifreq ifr;
strncpy(ifr.ifr_name, ifa->ifa_name, IFNAMSIZ - 1);
int mtu = 0;
if (ioctl(sockfd, SIOCGIFMTU, &ifr) == 0)
mtu = ifr.ifr_mtu;
/* IPv4 address */
if (ifa->ifa_addr->sa_family == AF_INET) {
char ip[INET_ADDRSTRLEN];
struct sockaddr_in *sin = (struct sockaddr_in *)ifa->ifa_addr;
inet_ntop(AF_INET, &sin->sin_addr, ip, sizeof(ip));
printf("%-12s %-6d %-18s %s\n", ifa->ifa_name, mtu, "N/A", ip);
}
}
freeifaddrs(ifap);
close(sockfd);
return 0;
}
Example 3: Set socket option to discover path MTU (avoid fragmentation)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* IP_MTU_DISCOVER, IP_PMTUDISC_DO */
int main(void)
{
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) { perror("socket"); return 1; }
/* Tell kernel: do Path MTU Discovery.
This sets the DF (Don't Fragment) bit on outgoing packets.
If a router can't forward the datagram without fragmenting,
it sends back an ICMP "fragmentation needed" message.
The kernel then reduces the effective MTU for this path. */
int val = IP_PMTUDISC_DO;
if (setsockopt(sockfd, IPPROTO_IP, IP_MTU_DISCOVER, &val, sizeof(val)) < 0) {
perror("setsockopt IP_MTU_DISCOVER");
return 1;
}
printf("Path MTU Discovery enabled on socket %d\n", sockfd);
/* After connecting to a destination, you can read the discovered MTU: */
/* int mtu; socklen_t len = sizeof(mtu);
getsockopt(sockfd, IPPROTO_IP, IP_MTU, &mtu, &len);
printf("Path MTU = %d\n", mtu); */
close(sockfd);
return 0;
}
Example 4: Check effective TCP segment size (MSS = MTU – headers)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h> /* TCP_MAXSEG */
int main(void)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) { perror("socket"); return 1; }
/* TCP Maximum Segment Size = effective payload per TCP segment.
For standard Ethernet: MSS = 1500 (MTU) - 20 (IP hdr) - 20 (TCP hdr) = 1460 bytes
This is the maximum data TCP puts in one segment to avoid IP fragmentation. */
int mss;
socklen_t len = sizeof(mss);
if (getsockopt(sockfd, IPPROTO_TCP, TCP_MAXSEG, &mss, &len) == 0)
printf("Default TCP MSS = %d bytes\n", mss);
/* You can also set MSS before connecting (affects SYN packet): */
int new_mss = 512; /* artificially small, for testing */
setsockopt(sockfd, IPPROTO_TCP, TCP_MAXSEG, &new_mss, sizeof(new_mss));
getsockopt(sockfd, IPPROTO_TCP, TCP_MAXSEG, &mss, &len);
printf("After setting MSS = %d bytes\n", mss);
close(sockfd);
return 0;
}
/* Output (typical):
Default TCP MSS = 536 bytes (before connection; will negotiate to 1460)
After setting MSS = 512 bytes */
MSS = MTU - IP header (20 bytes) - TCP header (20 bytes)
MSS = 1500 - 20 - 20 = 1460 bytes (standard Ethernet)
When you send data over TCP, the kernel splits it into segments of at most 1460 bytes to ensure each segment fits in one Ethernet frame without IP fragmentation.
| Concept | Quick Definition | Key Number/Fact |
|---|---|---|
| internet | Network of networks | Hides physical differences via single address format |
| TCP/IP origin | ARPANET / DARPA project | First shipped in 4.2BSD (1983) |
| Router | Forwards packets between subnets | Has one interface per connected subnet |
| Protocol layers | Application / Transport / Network / Data-link | Each layer adds header when sending data down |
| Encapsulation | Each layer wraps data from above in its own packet | Data → Segment → Datagram → Frame |
| Transparency | Each layer hides its complexity from layers above | Application sees a simple byte stream |
| Data-link layer | Device driver + NIC; handles physical link | Operates on frames; handles CRC, framing |
| MTU | Max IP payload size for one data-link frame | Ethernet = 1500 bytes; Loopback = 65536 bytes |
| TCP MSS | Max TCP payload per segment (avoids fragmentation) | 1500 – 20 – 20 = 1460 bytes |
netstat -i — the MTU column shows the MTU for each interface. Method 2: Use the ioctl system call with SIOCGIFMTU in a C program, passing an ifreq structure with the interface name. Additional: ip link show or cat /sys/class/net/eth0/mtu also work.IP_MTU_DISCOVER socket option.← Part 1: Internets ← Part 2: Protocol Layers
