Learn › Linux networking › IPv6 neighbors and path MTU
A real three-machine networking investigation with chapter-specific checks.
Problem: a small IPv6 ping can cross the network while a larger one loses the message that explains the path's size limit. First observe the healthy path, then create one bounded fault and restore the missing feedback without changing the management network. A small health check can pass while a larger transfer fails: each link has a packet-size limit, and the sender needs a useful message when a packet cannot fit.
Chapters 1 and 2 taught a next hop on a local link and the difference between a route lookup and a reply. Chapter 3 taught how one nftables rule can block a particular kind of packet. This chapter applies those ideas to IPv6. No tunnel, BGP or advanced routing configuration is required.
Use the named Client, Router and Server terminals. Confirm the terminal name before each exercise. Topology shows the assigned IPv6 subnets and exercise links; selecting a node does not switch the command target. This chapter has no verified Packets view for the IPv6 Packet Too Big exchange. Read live terminal probes and the chapter-owned firewall counter. A diagram or empty Packets view is not packet evidence.
Setup gives the machines unique local IPv6 addresses, routes and Router forwarding. IPv6 addresses have 128 bits and are written in colon-separated groups. A /64 prefix identifies the subnet used on one exercise link; the remaining bits identify an address on that link. These fd... addresses are private to this lab, so read this session's assignments rather than memorizing them. Router's link toward Server, eth2, has an MTU of 1280 bytes. MTU is the largest IP packet that link can carry, including its IPv6 header but not the Ethernet frame header. Client's eth1 can send 1500-byte IP packets. The path can carry no more than its smallest link allows.
eth0 carries management; keep its address, routes, MTU and Router Advertisement behavior intact. Router Advertisements can provide IPv6 network and default-route information, so a casual global forwarding change could affect management. Setup handles that state. Your fault changes only one chapter-owned Packet Too Big rule on Router.
The descriptor at /etc/lab/chapter.json supplies client_v6, server_v6, router_client_v6, router_server_v6 and lab_table for this session. Its chapter field must be 5. Read it on the relevant named terminal before using its addresses. Do not copy an IPv6 prefix from another session. On Client, the first read is:
python3 -m json.tool /etc/lab/chapter.json
machine: client prompt: admin@client:~$ answer: python3 -m json.tool /etc/lab/chapter.json output:
hint: Read the chapter descriptor before testing an IPv6 address.
On Client, ip -br -6 address shows the address on eth1. ip -6 route shows the subnet route through Router. On Router, compare both exercise interfaces and read eth2 MTU. On Server, read its eth1 address. A tentative address is still being checked for conflicts; a duplicate address conflicts with another machine. If either appears, stop and report it before testing reachability. These read commands do not change the network:
ip -br -6 address
ip -6 route
cat /sys/class/net/eth2/mtu # Router only
Before any fault, also record Router's management defaults so you can compare them at the end. These are reads, not changes:
ip -br -4 address show dev eth0
ip -br -6 address show dev eth0
ip -4 route show default
ip -6 route show default
cat /sys/class/net/eth0/mtu
sysctl -n net.ipv6.conf.eth0.accept_ra net.ipv6.conf.eth0.forwarding net.ipv6.conf.all.forwarding
machine: client prompt: admin@client:~$ answer: ip -br -6 address output:
hint: Find Client eth1 and compare it with client_v6 in the descriptor.
machine: router prompt: admin@router:~$ answer: cat /sys/class/net/eth2/mtu output:
hint: Router eth2 carries packets toward Server. Its MTU should be 1280.
Ask Client for the route to the actual server_v6 address. The lookup identifies the outgoing interface and Router next hop. It does not prove Server received a packet.
IPv6 Neighbor Discovery lets a machine learn a nearby next hop's link-layer, or MAC, address. It uses ICMPv6 messages for this local-link work, rather than IPv4 ARP. A destination on another subnet is not Client's local neighbor. Client needs Router's address on the Client link first; Router later makes a separate decision on its Server link. ip -6 neigh show dev eth1 reads what Client currently knows about nearby IPv6 neighbors. It cannot by itself prove Server received a packet.
>>> The route chooses Router as the next hop. The neighbor table can show that local resolution; a successful reply provides separate end-to-end evidence.
Use the descriptor's client_v6 and server_v6 in a two-probe test. First assign these shell variables from this session's descriptor on Client. $(...) runs the command inside the parentheses and stores its printed result in a shell variable. Here Python reads two named fields from the JSON descriptor; you do not need to write Python. The variables live only in that Client shell, so assign them again if you open a new shell. Read each line before entering its matching exercise:
CLIENT_V6=$(python3 -c 'import json; print(json.load(open("/etc/lab/chapter.json"))["client_v6"])')
SERVER_V6=$(python3 -c 'import json; print(json.load(open("/etc/lab/chapter.json"))["server_v6"])')
ip -6 route get "$SERVER_V6" from "$CLIENT_V6"
ping -6 -n -I "$CLIENT_V6" -c 2 -W 2 -s 100 "$SERVER_V6"
ip -6 neigh show dev eth1
In the ping command, -6 selects IPv6, -I selects Client's source address, -c 2 sends two requests, -W 2 bounds each reply wait, and -s 100 sends 100 bytes of echo data. -n keeps addresses numeric so a DNS lookup does not distract from the path test.
machine: client prompt: admin@client:~$ answer: CLIENT_V6=$(python3 -c 'import json; print(json.load(open("/etc/lab/chapter.json"))["client_v6"])') output:
hint: Assign the source address from Client's chapter descriptor.
machine: client prompt: admin@client:~$ answer: SERVER_V6=$(python3 -c 'import json; print(json.load(open("/etc/lab/chapter.json"))["server_v6"])') output:
hint: Assign the destination address from this session's descriptor.
machine: client prompt: admin@client:~$ answer: ip -6 route get "$SERVER_V6" from "$CLIENT_V6" output:
hint: Find the next hop and outgoing interface for this remote subnet.
machine: client prompt: admin@client:~$ answer: ping -6 -n -I "$CLIENT_V6" -c 2 -W 2 -s 100 "$SERVER_V6" output:
hint: Send two small IPv6 echo requests from Client's assigned address.
machine: client prompt: admin@client:~$ answer: ip -6 neigh show dev eth1 output:
hint: Identify the next hop on Client's link, not Server's remote address.
Record the actual exit status, replies and neighbor entry. A STALE entry can still be usable; it is not proof of a failed route. A small reply shows this packet size worked at this moment, not that every larger packet will.
Router must send the packet toward Server through its 1280-byte eth2 link. The -s value is echo data, not total IP packet size. A ping with 1400 bytes of echo data also has a 40-byte IPv6 header and an 8-byte ICMPv6 header, making an ordinary 1448-byte IPv6 packet before any extension headers. That exceeds 1280. IPv6 routers do not fragment it to make it fit. Router should send Client an ICMPv6 Packet Too Big message stating the usable MTU. Client can use that feedback to choose smaller packets later. This is path MTU discovery. The design depends on returning useful control messages: allowing small traffic while silently dropping this feedback can leave larger transfers failing.
>>> The probe is designed to exceed the link limit. A report explains the limit; a timeout only says this attempt did not produce the expected feedback.
On Client, first read the complete oversized-probe command. -M do asks the Linux ping program to use path-MTU checks and not fragment this probe locally. It does not guarantee that a packet leaves Client: after the first Packet Too Big response, Client may remember the smaller path limit and reject a repeated probe locally. The initial 1448-byte IPv6 packet fits Client's 1500-byte link but is too large for Router's 1280-byte outgoing link. -c 1 sends one request, so record both the message and the exit status even when it is nonzero.
ping -6 -n -I "$CLIENT_V6" -c 1 -W 2 -M do -s 1400 "$SERVER_V6"
machine: client prompt: admin@client:~$ answer: ping -6 -n -I "$CLIENT_V6" -c 1 -W 2 -M do -s 1400 "$SERVER_V6" output:
hint: Record whether feedback came from Router or the local kernel, and its reported MTU.
Here a nonzero ping exit can contain the evidence you need. Read the message and its origin. A later probe to the same destination can use cached path-MTU knowledge and fail locally with a size error before a fresh packet leaves Client. The exercise checks the live path automatically. Save its healthy baseline before introducing a fault. A healthy starting path does not finish this investigation: you will also observe blocked feedback and verify its repair. The checker uses a fresh temporary Server destination, waits until IPv6 confirms that the address is not duplicated, then removes it. Its live result is a separate observation; do not describe your repeated manual probe as equivalent.
The guided fault adds one chapter-owned nftables output rule on Router. It matches Packet Too Big messages leaving eth1 for this Client and drops them. Small echo traffic is outside that exact match. Before changing policy, predict which result should change: the small Client reply, the fresh oversized check, or both. Write down why.
On Router, load the current session's table and Client address from its descriptor, then inspect the empty owned chain. labnet is a Linux Camp client that asks a restricted broker to run the reviewed rule commands. It is not a standard Linux command or a general root shell. The setup created this session's table and chain; you change only one rule inside them.
Use the four Router exercises below in order. The first two load the session's names, the third inspects the chain, and the fourth adds the fault once.
Run these lines in the Router shell, one exercise at a time. LC_TABLE names only this session's chapter-owned nftables table. CLIENT_V6 names the Client address to which Router would send size feedback. The chain listing should have no rules before the change. The final line matches only Packet Too Big messages leaving eth1 for that Client; counter lets you inspect matches and the comment identifies the owned rule during repair.
LC_TABLE=$(python3 -c 'import json; print(json.load(open("/etc/lab/chapter.json"))["lab_table"])')
CLIENT_V6=$(python3 -c 'import json; print(json.load(open("/etc/lab/chapter.json"))["client_v6"])')
labnet nft -a list chain inet "$LC_TABLE" output
labnet nft add rule inet "$LC_TABLE" output oifname eth1 ip6 daddr "$CLIENT_V6" icmpv6 type packet-too-big counter drop comment lc5-pmtu-block
machine: router prompt: admin@router:~$ answer: LC_TABLE=$(python3 -c 'import json; print(json.load(open("/etc/lab/chapter.json"))["lab_table"])') output:
hint: Read lab_table from this Router session’s descriptor.
machine: router prompt: admin@router:~$ answer: CLIENT_V6=$(python3 -c 'import json; print(json.load(open("/etc/lab/chapter.json"))["client_v6"])') output:
hint: Variables belong to one shell. Router needs its own CLIENT_V6 value.
machine: router prompt: admin@router:~$ answer: labnet nft -a list chain inet "$LC_TABLE" output output:
hint: The output chain should be empty before you add the fault.
machine: router prompt: admin@router:~$ answer: labnet nft add rule inet "$LC_TABLE" output oifname eth1 ip6 daddr "$CLIENT_V6" icmpv6 type packet-too-big counter drop comment lc5-pmtu-block output:
hint: Wait for the automatic fresh probe to observe the blocked feedback. Do not remove the rule yet.
Confirm the chain now has only that owned rule. The oifname eth1 and Client destination narrow the drop to feedback returning across the exercise link; the rule does not block all ICMPv6. The exercise automatically checks the fault after the command finishes. Wait for its completion mark before repairing the rule: it requires an actual increase in the owned drop counter, a missing fresh size report and small replies that still work. The checker uses a fresh destination so its missing Packet Too Big result can be compared with your healthy baseline. Do not create or delete the table or chain, change eth0, or use unrestricted sudo.
Repeat the small ping on Client. On Router, read the owned chain and its counter after the automatic fresh probe. Compare it with your earlier counter reading.
On Client, use the variables you loaded there in 5.3:
ping -6 -n -I "$CLIENT_V6" -c 2 -W 2 -s 100 "$SERVER_V6"
On Router, use LC_TABLE loaded there in 5.6:
labnet nft -a list chain inet "$LC_TABLE" output
The small reply checks basic reachability while the fault remains installed. A higher counter on the owned rule supports that the fresh size-feedback check matched this rule; it does not turn the small ping into proof about large packets.
machine: client prompt: admin@client:~$ answer: ping -6 -n -I "$CLIENT_V6" -c 2 -W 2 -s 100 "$SERVER_V6" output:
hint: Keep the rule installed while checking the small-packet control.
machine: router prompt: admin@router:~$ answer: labnet nft -a list chain inet "$LC_TABLE" output output:
hint: Find the Packet Too Big rule’s counter and handle. Explain what its increase supports.
A positive counter delta supports that this exact rule matched feedback. It does not prove every packet's interface path. If the counter does not move, reconsider the match and probe timing. Do not count a cached local size error as a new dropped Router message.
>>> The changed result narrows the problem to a size-dependent exchange. The owned drop counter and current rule make the policy explanation testable.
On Router, inspect the session-owned chain again and identify only the rule that blocks Packet Too Big feedback. Explain why its match and counter fit the observations. Read the number shown after handle on that rule. The read command below prompts for that number; type only the number you just saw. Then remove only that rule:
labnet nft -a list chain inet "$LC_TABLE" output
read -r -p 'Owned drop rule handle: ' OBSERVED_HANDLE
labnet nft delete rule inet "$LC_TABLE" output handle "$OBSERVED_HANDLE"
labnet nft -a list chain inet "$LC_TABLE" output
The handle is the number from the owned rule you just inspected, not a number from another table or session. After entering it, use the repair exercise below to delete that one rule. The final exercise checks that the chain remains present and is empty.
machine: router prompt: admin@router:~$ answer: labnet nft delete rule inet "$LC_TABLE" output handle "$OBSERVED_HANDLE" output:
hint: Set OBSERVED_HANDLE using the read command above, then remove that exact owned rule.
machine: router prompt: admin@router:~$ answer: labnet nft -a list chain inet "$LC_TABLE" output output:
hint: Confirm the rule is absent while the existing chain remains.
The chain should now be empty. Do not flush the ruleset or change eth0, the 1280-byte link MTU, all ICMPv6, or unrelated Router policy. The repair exercise automatically checks the new state. Five current-state checks require small replies, a fresh Packet Too Big report naming 1280, the unchanged Router bottleneck, the blocking policy removed while other policy survives, and unchanged management state. A sixth check records that this same session actually observed the blocked-feedback fault before repair. Compare the current results with the healthy baseline and note any failed or inconclusive check. A passing state describes this test window; it does not grade your diagnosis.
Later, suppose a different path has a 1400-byte bottleneck and the required feedback arrives correctly. Here 1400 is the link MTU, not the ping -s data size. The same -s 1400 probe totals 1448 IPv6 bytes before extension headers. That link limit is smaller than Client's 1500-byte link but larger than this chapter's 1280-byte bottleneck. IPv6 requires a link MTU of at least 1280 bytes. Predict what a fresh oversized probe would report and whether the owned chain should contain a Packet Too Big drop. If feedback arrives, explain why leaving firewall policy unchanged is the justified decision even though that oversized echo cannot cross the link as sent. This is a reasoning plan, not an executed changed case. Keep your prediction with your notes so you can compare it with measurements in a later investigation.
Save Client and Router addresses, the neighbor and route decision, both packet sizes, actual error origin and MTU, rule and counter changes, and post-repair checks. State that a short ICMPv6 test does not establish every application's later behavior. The chapter is an introduction to Linux IPv6 path behavior; deeper routing and tunnels belong in later courses.
Compare management addresses, default routes, eth0 MTU and Router Advertisement settings with the values recorded in 5.1. Setup made the router changes needed for this exercise; do not alter global IPv6 forwarding to make the check pass. If those properties changed, report the fixture mismatch before ending the session.
References: RFC 4861, Neighbor Discovery, [RFC 8201, IPv6 Path MTU Discovery](https://www.rfc-editor.org/info/rf
Practice Chapter 5. IPv6 neighbors and path MTU in a real Linux terminal at The Linux Camp. Progress is verified automatically as you type commands on the machine.