Sockets – Fundamentals of TCP/IP Networks The Data-Link Layer and MTU

 

Chapter 58: Sockets – Fundamentals of TCP/IP Networks
Part 3 of 3  |  The Data-Link Layer and MTU
🔗 Data-Link Layer
📏 MTU
🖼 Frames

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).

Key Terms
data-link layer frame frame header MTU maximum transmission unit network interface device driver acknowledgement error detection flow control fragmentation netstat -i Ethernet

1. What is the Data-Link Layer?

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).

Where the Data-Link Layer Sits
Application Layer (your code)
Transport Layer (TCP / UDP)
Network Layer (IP)
➡ Data-Link Layer (device driver + NIC) ⬅
Physical Medium (cable / Wi-Fi / fiber)

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.

2. Frames — The Data Unit of the Data-Link Layer

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.

Structure of an Ethernet Frame
Dest MAC
6 bytes
Src MAC
6 bytes
EtherType
2 bytes
Payload
(IP Datagram)
46–1500 bytes
CRC
4 bytes
Dest MAC: Hardware address of next hop Src MAC: Hardware address of sender NIC EtherType: 0x0800=IPv4, 0x86DD=IPv6 CRC: Error detection checksum

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
Key point for socket programmers: From an application point of view, you can completely ignore the data-link layer. The kernel device driver handles all framing, CRC, and hardware interaction. You never write frame headers in socket code.

3. MTU — Maximum Transmission Unit

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 Fragmentation when Datagram > MTU
Original IP Datagram: 3000 bytes

3000 bytes → too big for Ethernet MTU 1500
↓ IP fragments into 2 datagrams
Fragment 1
IP header + 1480 bytes data

= 1500 bytes total ✓

Fragment 2
IP header + 1500 bytes data

= 1520 bytes total ✓

↓ Each fragment goes into its own Ethernet frame
Destination reassembles the fragments back into the original datagram
Path MTU Discovery (PMTUD): Modern TCP implementations use PMTUD to find the smallest MTU along the path from sender to receiver. TCP then limits its segment size to avoid IP fragmentation entirely. This is why you might see TCP segment sizes of 1460 bytes (1500 MTU – 20 IP header – 20 TCP header = 1460).

4. Viewing Network Interfaces and MTUs in Linux

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

5. Code Examples: Working with MTU in Socket Programs

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  */
MTU / MSS formula to remember:

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.

6. Chapter 58 Summary: Key Facts at a Glance
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

Interview Questions
Q1. What is the data-link layer and what are its two main components?
Answer: The data-link layer is the lowest layer of the TCP/IP software stack. Its two components are: (1) the device driver in the kernel, which implements the protocol logic, and (2) the network interface hardware (NIC), which transmits and receives signals on the physical medium. Together, they transfer data across a single physical link.
Q2. What is a frame? How does it differ from an IP datagram?
Answer: A frame is the unit of data at the data-link layer. When IP passes a datagram down to the data-link layer, the data-link layer encapsulates it in a frame by adding a frame header (destination MAC address, source MAC address, frame type) and typically a trailer (CRC checksum). The IP datagram is the unit at the network layer — a frame contains one IP datagram as its payload.
Q3. What is MTU? What is the typical MTU for Ethernet?
Answer: MTU (Maximum Transmission Unit) is the largest IP datagram payload that the data-link layer can carry in a single frame. The typical Ethernet MTU is 1500 bytes. The loopback interface has an MTU of 65536 bytes. Different data-link technologies have different MTUs.
Q4. What happens when an IP datagram is larger than the MTU?
Answer: IP fragments the datagram into multiple smaller datagrams, each small enough to fit in one frame. Each fragment gets its own IP header with fragmentation flags and offsets. The destination IP layer reassembles the fragments back into the original datagram before passing data to TCP/UDP. Fragmentation adds overhead and can cause performance issues; modern TCP uses Path MTU Discovery to avoid it.
Q5. What is TCP MSS and how is it related to MTU?
Answer: MSS (Maximum Segment Size) is the maximum amount of application data TCP will put in a single TCP segment. It is derived from the MTU: MSS = MTU – IP header (20 bytes) – TCP header (20 bytes). For Ethernet, MSS = 1500 – 20 – 20 = 1460 bytes. By staying within the MSS, TCP ensures each segment fits in one Ethernet frame, avoiding IP fragmentation entirely.
Q6. Does the data-link layer always provide reliability (retransmission, flow control)?
Answer: No. Ethernet, the most common data-link technology, does not provide acknowledgements, retransmission, or flow control at the frame level. These are left to higher layers — TCP provides reliability at the transport layer. Some other data-link protocols (like older dial-up PPP) do include per-frame acknowledgement and retransmission, but modern wired and wireless networks generally do not.
Q7. How would you find the MTU of a network interface on Linux? (Two methods)
Answer: Method 1: Run 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.
Q8. Why can application programmers generally ignore the data-link layer?
Answer: Because the kernel device driver handles all data-link layer details — framing, CRC calculation and checking, hardware timing, MAC address resolution (via ARP), and interface-specific quirks. Application code interacts only with the socket API (read/write), which is far above the data-link layer. The only data-link concept that occasionally affects application design is the MTU, because it influences IP fragmentation and TCP MSS.
Q9. What is Path MTU Discovery and why is it useful?
Answer: Path MTU Discovery (PMTUD) is a technique where TCP/IP discovers the smallest MTU along the entire path from source to destination. It works by sending packets with the DF (Don’t Fragment) bit set. If a router cannot forward a packet without fragmenting, it sends back an ICMP “Fragmentation Needed” message with the link’s MTU. The sender then reduces its effective MTU and TCP MSS for that path. This eliminates IP fragmentation, improving performance. On Linux, you can control it with IP_MTU_DISCOVER socket option.

Chapter 58 Navigation

← Part 1: Internets ← Part 2: Protocol Layers

Next chapter will cover Internet domain sockets (Ch. 59)

Leave a Reply

Your email address will not be published. Required fields are marked *