Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

intermediatevyos-static~30 min

Static Route Exists but Traffic Fails

Reported symptoms

  • Everything the DC sends into the payments zone (10.90.0.0/16) began timing out at 08:05 on Monday. The sessions hang until the client gives up rather than being refused, which reads as a far-end problem
  • Traffic the payments zone initiates towards the DC is perfect. The 08:15 file drop landed, and a monitoring poll from a host inside 10.90.0.0/16 has never missed a sample
  • The firewall team replaced the payments firewall chassis at the weekend, kept the same addresses, and validated their side. Their new chassis shows zero packets received from `edge1` — not denied, not dropped, none at all
  • `show ip route 10.90.0.0/16` on `edge1` shows `S>*` with an egress interface. Selected, installed, unchanged for eleven weeks
  • `ping 192.0.2.20` from `edge1` reports 100% packet loss with no error message. `ping 192.0.2.1`, the other router on the same VLAN, is fine, and a ping from the firewall to `edge1` is fine
  • Nothing has been committed on `edge1` for eleven weeks, and the two other devices on the same transit VLAN are unaffected

Evidence

  • · `show ip route 10.90.0.0/16` — `S>* 10.90.0.0/16 [1/0] via 192.0.2.20, eth1, 11w2d`, with both the selected and the installed markers present
  • · `ip route get 10.90.4.11` — the kernel resolves payments traffic to `via 192.0.2.20 dev eth1`, so the forwarding decision is the intended one
  • · `ping -c 3 192.0.2.20` — 100% packet loss, and notably no `Destination Host Unreachable` line
  • · `ip neigh show 192.0.2.20 dev eth1` — one entry, `lladdr 52:54:00:9d:1a:7c PERMANENT`
  • · `ip neigh show dev eth1` — every other neighbour on the segment is `REACHABLE` or `STALE`; this is the only `PERMANENT` entry on the router
  • · `tcpdump -nei eth1 arp` for sixty seconds while the pings fail — ARP for other hosts on the VLAN, and not one request for 192.0.2.20
  • · `arping -I eth1 -c 3 192.0.2.20` — three replies, from `52:54:00:c3:0e:41`, which is not the address in the neighbour entry
  • · `show configuration commands | match arp` — nothing. No static ARP node exists in the configuration
  • · `show system commit | head -3` — the most recent commit on `edge1` is eleven weeks old
  • · The change record for NET-4471, June: "pinned the firewall MAC with ip neigh replace to get traffic moving; remove once the segment is fixed"
Diagnosis and resolutionclick to reveal

Root cause

The kernel is building frames for the payments next hop with a destination MAC that no longer exists on the segment, and it will never correct itself because the entry was installed by hand as a permanent one. In June, during an unrelated fault on this VLAN, an engineer ran `ip neigh replace 192.0.2.20 lladdr ... dev eth1` from the shell to get traffic moving. That form of the command, with a link-layer address and no explicit `nud` state, installs the entry as PERMANENT, and a permanent entry is exactly what its name says: the kernel never ages it, never probes it, and never replaces it with what the wire says. It is not a cache entry, it is an assertion. At the weekend the firewall chassis was replaced. The addresses were kept, and the new NIC has a new MAC — `52:54:00:c3:0e:41`, which `arping` gets on the first try because `arping` puts its own request on the wire rather than reading the cache. Every other device on the VLAN re-learned the new MAC within seconds of the first frame, because their entries were dynamic. `edge1` did not, because its entry is not allowed to change. Three things then conspire to make this hard to see. The routing is genuinely correct, so every command a routing engineer reaches for returns a healthy answer: the static is selected and installed, and the kernel agrees with it. The failure is one layer below the lowest layer that `show ip route` can describe. Second, the direction that still works is not a coincidence: the firewall resolves `edge1` by ARP like any other host, gets a correct answer, and so everything the payments zone initiates arrives normally — one-way reachability across a shared segment is the signature of a mapping problem rather than a path problem. Third, the far end can offer no evidence at all. A frame addressed to a MAC the chassis does not own is discarded by the NIC before any software on it is aware a packet arrived, so "we see nothing from you, not even drops" is a true statement that sounds like an accusation. The pin was also invisible to every place an operator looks for changes. It is not in the configuration, so `show configuration commands`, `compare`, the nightly configuration backup and the config-as-code diff are all silent about it — the same blind spot as a hand edit made in `vtysh`. And it does not survive a reboot, which means it was never a fix. It was a fault with a delayed fuse, waiting for either a reboot to erase it or a hardware change to make it wrong.

Remediation

Prove the segment can resolve the address before you take the pin away, then delete the pin and let ARP do its job. The order matters: the June note claims ARP on this VLAN was broken, and if that were still true, removing the override would convert a one-way blackhole into a two-way one during an incident. `arping -I eth1 -c 3 192.0.2.20` answers that question in three seconds and hands you the correct MAC as a side effect, because it bypasses the neighbour table entirely. With an answer in hand, `ip neigh del 192.0.2.20 dev eth1` removes the override; the next packet towards the payments zone triggers a normal ARP request, the reply installs a dynamic entry, and traffic resumes. The cost is a few queued packets during resolution, which on a connected segment is sub-second. Refuse the obvious shortcut: pinning the new MAC with the same command that caused this rearms the identical trap and hands its fuse to whoever next replaces that chassis. Refuse the broad version too — `ip neigh flush dev eth1` on a transit interface discards every neighbour on the segment, including the BGP peer's, and forces a burst of re-resolution and a brief queue for every next hop on it, to fix one entry that a targeted delete would have fixed with no blast radius. If `arping` returns nothing, stop: the pin is now the only thing that was ever making this path work, this is a segment or VLAN fault rather than a routing one, the owner is the team that runs the switch, and the correct move is to hold the override in place with an explicit end time on the ticket rather than to remove it and be blind as well as broken. Whichever branch you take, sweep the rest of the fleet before closing: `ip neigh show nud permanent` on every router, and treat every entry that is not backed by a configuration node as the same landmine with a different fuse.

Verification

Check the entry, not the ping. `ip neigh show 192.0.2.20 dev eth1` must show `REACHABLE` or `STALE` with a link-layer address matching the one `arping` reported, and the word PERMANENT must be gone — a ping that succeeds tells you the mapping is right now, and the state tells you the kernel is allowed to keep it right. `ip neigh show nud permanent` on `edge1` should return nothing at all, because no configuration node on this router asks for a static ARP entry. Then verify the traffic from somewhere that is not the router: a real client in the DC opening a real session into 10.90.0.0/16, because the router pinging its own next hop tests one frame and not the path. Get independent confirmation from the far end — the firewall's inbound counters for `edge1` are the only evidence in this incident that is not produced by the box that produced the fault, and their silence was the sharpest symptom available. Prove the check can fail rather than trusting that it passed: on a lab router, install a permanent entry with a deliberately wrong MAC and confirm that the sweep command reports it and that your monitoring notices, so you know what the failing state looks like on the check you are now relying on. Finally, watch the wire once: `tcpdump -nei eth1 arp` should now show a request for 192.0.2.20 when the entry ages, and an answer. Silence there was the finding, and silence there again would mean the pin is back.

Prevention

Nothing that changes forwarding may live outside the configuration tree without an owner and an end time. VyOS has `commit-confirm` precisely because an unattended change needs a deadline; a command typed into the shell has no such mechanism, so the deadline has to be a human one, written on the ticket, with a name against it. If an override is worth applying at 02:00, it is worth a five-line note in the change record and a follow-up task — and if it cannot be expressed in the configuration at all, that is a gap to raise, not a gap to paper over. Sweep for the class rather than the instance: `ip neigh show nud permanent` should be empty on every router that has no static ARP in its configuration, and that is a one-line check that would have raised this in June rather than in August. Treat a device replacement on a shared segment as a change on every device on that segment. The team doing the swap validates their own box and cannot see the neighbours' view of it, so the post-change check belongs on the routers, and it is a MAC diff: capture `show arp` before and after, and compare. Test both directions, always. One-way reachability is the signature of a Layer 2 mapping fault, and a ping run from one end can not see it — the fact that the payments zone could reach the DC was treated as evidence of health for the first hour, when it was the most specific clue in the ticket. And keep the two questions apart in your own habits: whether the router has chosen the right next hop is `show ip route`, and whether it can address a frame to that next hop is `ip neigh`. Only the second one is about this class of fault.

Reported symptoms

edge1 is the DC router that faces the payments zone. It has one static route that matters here — 10.90.0.0/16 via 192.0.2.20, the inside address of the payments firewall pair — and 192.0.2.20 sits on the transit VLAN that edge1 reaches through eth1. There is no recursion involved: the next hop is on a directly connected subnet.

At the weekend the firewall team replaced that chassis. Same addresses, same cabling, same policy, new hardware. They validated their side on Sunday afternoon and closed the window.

At 08:05 on Monday every batch job that writes into the payments zone began timing out. Timing out, not being refused — the sessions hang until the client gives up, which is what a far-end problem looks like from a log file.

Three facts arrived over the next hour, and none of them pointed at edge1:

  • Traffic in the other direction is perfect. The 08:15 file drop from the payments zone landed on time, and a monitoring poll originating inside 10.90.0.0/16 has not missed a single sample all morning. Whatever is broken, it is not the link and not the firewall’s policy.
  • The firewall team’s new chassis reports zero packets received from edge1. Not denied, not dropped by a rule, none at all — while its counters for the other devices on the same VLAN increment normally.
  • Nothing changed on edge1. show system commit goes back eleven weeks, and the routing is verifiably the same routing that worked on Friday.

On the router itself, the first two checks both came back healthy. The route is in the table with its markers. The kernel agrees with the route. By 09:00 the working theory was that the replacement chassis had a policy defect, because the local routing had been confirmed twice by two people.

Evidence provided

Read-only / Safeselected, installed, and eleven weeks old — this route is not the fault
vyos@edge1:~$ show ip route 10.90.0.0/16
Codes: K - kernel route, C - connected, S - static, R - RIP,
     O - OSPF, B - BGP, > - selected route, * - FIB route

S>* 10.90.0.0/16 [1/0] via 192.0.2.20, eth1, 11w2d

Illustrative output

Read-only / Safethe thing that actually decides, agreeing with the thing that was configured
vyos@edge1:~$ ip route get 10.90.4.11
10.90.4.11 via 192.0.2.20 dev eth1 src 192.0.2.2 uid 1000
  cache

Illustrative output

Read-only / Safeloss with no error line — read what is missing here, not what is present
vyos@edge1:~$ ping -c 3 192.0.2.20
PING 192.0.2.20 (192.0.2.20) 56(84) bytes of data.

--- 192.0.2.20 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2043ms

Illustrative output

Read-only / Safetwo neighbours the kernel maintains, and one it has been told
vyos@edge1:~$ ip neigh show dev eth1
192.0.2.1 lladdr 52:54:00:1b:8f:20 REACHABLE
192.0.2.9 lladdr 52:54:00:44:d1:02 STALE
192.0.2.20 lladdr 52:54:00:9d:1a:7c PERMANENT

Illustrative output

Read-only / Safesixty seconds of ARP while the pings fail, and not one request for 192.0.2.20
vyos@edge1:~$ sudo tcpdump -nei eth1 -c 20 arp
09:41:02.118447 52:54:00:1b:8f:20 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806),
Request who-has 192.0.2.2 tell 192.0.2.1, length 46
09:41:02.118501 52:54:00:0e:33:a1 > 52:54:00:1b:8f:20, ethertype ARP (0x0806),
Reply 192.0.2.2 is-at 52:54:00:0e:33:a1, length 28
09:41:07.664190 52:54:00:44:d1:02 > ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806),
Request who-has 192.0.2.1 tell 192.0.2.9, length 46

Illustrative output

Read-only / Safethe segment answers immediately — with a different MAC from the one in the table
vyos@edge1:~$ sudo arping -I eth1 -c 3 192.0.2.20
ARPING 192.0.2.20 from 192.0.2.2 eth1
Unicast reply from 192.0.2.20 [52:54:00:C3:0E:41]  0.712ms
Unicast reply from 192.0.2.20 [52:54:00:C3:0E:41]  0.688ms
Unicast reply from 192.0.2.20 [52:54:00:C3:0E:41]  0.701ms
Sent 3 probes (1 broadcast(s))
Received 3 response(s)

Illustrative output

Read-only / Safewhatever installed that entry, the configuration did not
vyos@edge1:~$ show configuration commands | match arp

Work the evidence before reading on

Every routing command on this router returns a healthy answer, and the traffic still does not arrive. That combination is the whole exercise.

  1. The route is selected and installed, and ip route get confirms the kernel will send payments traffic out eth1 towards 192.0.2.20. What has to happen to a packet after the forwarding decision is made and before it reaches the wire, and which command shows you that step?
  2. Read the ping output for what is not in it. A router that cannot resolve its next hop prints something specific and prints it fast. This one printed nothing but loss. What does that difference tell you about whether the packets were sent?
  3. Three neighbour entries, two of which the kernel is maintaining and one of which it is not. What does the third state mean, what does the kernel do to the first two that it will never do to the third, and which of those behaviours would have fixed this by itself?
  4. Sixty seconds of packet capture produced ARP for two other hosts and not one request for 192.0.2.20, during a period when pings to that address were failing every second. Silence is the strongest evidence in this ticket — what does it prove about what the kernel believes?
  5. arping gets an answer in under a millisecond from a MAC that does not appear in the neighbour table. Why does that tool succeed where ping failed, and what is the difference between what the two commands consult?

Then the question that resolves it: traffic from the payments zone to the DC works perfectly. On a shared segment, what single kind of fault breaks one direction and leaves the other untouched?

Root cause

1. A hand-installed neighbour entry, and PERMANENT means what it says

In June this VLAN had a fault, and an engineer got traffic moving again by pinning the firewall’s MAC from the shell:

Configuration changethe command from the June change record — note what it does not say
vyos@edge1:~$ sudo ip neigh replace 192.0.2.20 lladdr 52:54:00:9d:1a:7c dev eth1

With a link-layer address and no explicit nud state, that installs the entry as PERMANENT. A permanent entry is not a cache line, it is an assertion. The kernel never ages it, never probes it, and never lets an ARP reply overwrite it. It stays exactly as typed until somebody deletes it or the box reboots.

At the weekend the chassis was replaced. Same address, new NIC, new MAC. Every other device on that VLAN learned the new MAC within seconds of the first frame, because their entries are dynamic and dynamic entries are revalidated. edge1 did not, because its entry is not permitted to change.

So the router is doing exactly what it was told: encapsulating every packet for 10.90.0.0/16 in a frame addressed to 52:54:00:9d:1a:7c, a MAC that is now sitting in a box on a shelf.

2. Every layer a routing engineer can see is healthy

This is why two people confirmed the routing and moved on. The static route is in the RIB, selected, installed in the FIB, and ip route get — the kernel’s own answer — names the right interface and the right next hop. Nothing in the routing subsystem is wrong, and no amount of staring at show ip route will find this, because address resolution happens after the routing decision and show ip route has no visibility into it.

The ping output carries the tell, in what it does not print. When a next hop cannot be resolved at all, the local kernel gives up and ping reports Destination Host Unreachable from the router’s own address, usually within a second. Here there was no such line: just three probes sent, nothing received. The packets went out. They were addressed to nobody.

3. Silence on the wire is the finding

Sixty seconds of tcpdump -nei eth1 arp during active failure produced ARP for two other hosts and not a single request for 192.0.2.20.

That is the most decisive piece of evidence in the incident, and it is a negative. A router that cannot resolve a next hop is noisy about it — the kernel retries, and the requests are all over the capture. Silence means the kernel is not asking, and the only reason it would not ask is that it believes it already knows.

arping cuts through it because it does not consult the neighbour table at all. It builds an ARP request itself, puts it on the interface, and reports what comes back, which is why it returned the correct MAC on the first probe while ping was still failing.

4. Why the other direction never broke, and why the far end saw nothing

The firewall resolves edge1 the ordinary way: it ARPs, edge1 answers, and it caches a correct entry. Nothing about this fault touches that path, so every session the payments zone initiates works perfectly — including the file drop and the monitoring poll that were both being read as evidence of health.

One-way reachability across a shared segment is the signature of a mapping problem rather than a path problem, and it is worth learning as a shape: cabling, VLANs and firewall policy tend to break both directions, whereas an address-to-MAC mapping is per-device and per-direction.

The far end’s evidence is a second consequence of the same mechanism. A frame addressed to a MAC the chassis does not own is discarded by the NIC’s own filter before any software on it is aware that a packet arrived. The switch may even flood it to their port, since no entry for the dead MAC exists in its forwarding database, and the chassis will still count nothing. “We see nothing from you, not even drops” was true, precise, and unhelpful, because it sounds like an accusation and is actually a symptom.

Resolution

  1. Prove the segment can still resolve the address before you touch anything. arping -I eth1 -c 3 192.0.2.20 bypasses the neighbour table, answers in three seconds, and hands you the current MAC as a side effect.
  2. If arping returns nothing, stop and re-scope. The pin is then the only thing that was ever making this path work, the fault is on the segment rather than in routing, the owner is the switch team, and the override stays in place with an explicit end time on the ticket.
  3. With an answer in hand, delete the override and only the override: ip neigh del 192.0.2.20 dev eth1. The next packet towards the payments zone triggers a normal ARP request and the reply installs a dynamic entry.
  4. Refuse the shortcut of pinning the new MAC. ip neigh replace with the current address would restore traffic in one command and rearm the identical trap, with its fuse handed to whoever next replaces that chassis.
  5. Refuse the broad flush. ip neigh flush dev eth1 on a transit interface discards every neighbour on the segment, including the BGP peer, and buys a burst of re-resolution and a brief queue on every next hop to fix one entry.
  6. Confirm traffic from a real client rather than from the router, then ask the firewall team to confirm inbound packets from edge1 on their counters.
  7. Sweep the fleet before closing: ip neigh show nud permanent on every router. Any entry there that no configuration node asks for is the same landmine with a different fuse.
  8. Close NET-4471 properly. The June note said the pin should be removed; the reason it survived eight weeks is that saying so on a ticket is not a mechanism.

Verification

  1. ip neigh show 192.0.2.20 dev eth1 reports REACHABLE or STALE with the MAC that arping returned. The state is the test, not the ping: a correct mapping today plus a state the kernel may maintain is what makes it stay correct.
  2. ip neigh show nud permanent on edge1 returns nothing, because no configuration node on this router asks for a static ARP entry.
  3. A real client in the DC opens a real session into 10.90.0.0/16. The router pinging its own next hop tests one frame on one segment; it is not the same claim.
  4. The firewall reports inbound packets from edge1 on its counters. That is the only evidence in this incident not produced by the box that produced the fault.
  5. tcpdump -nei eth1 arp now shows a request for 192.0.2.20 when the entry ages, and an answer to it. Silence there was the finding; silence again would mean the pin is back.
  6. The check can fail. On a lab router, install a permanent entry with a deliberately wrong MAC and confirm the sweep reports it, so you know what the failing state looks like on the check you now depend on.
  7. The other two devices on the transit VLAN still forward normally, confirming that nothing in the resolution touched neighbours that were never part of the fault.

Prevention

  • Give every out-of-tree change an owner and an end time. VyOS has commit-confirm because an unattended configuration change needs a deadline; a command typed into the shell has no equivalent, so the deadline has to be written on the ticket with a name against it.
  • Sweep for the class, not the instance. ip neigh show nud permanent should be empty on any router whose configuration contains no static ARP, and that check would have raised this in June rather than in August.
  • Treat a device replacement on a shared segment as a change on every device on that segment. The team doing the swap can validate only their own box; the neighbours’ view of it is checked on the neighbours, and the check is a MAC diff — show arp before, show arp after, compare.
  • Record MACs in the change record for a chassis swap. A diff is a fact; a recollection of what the old MAC was is an argument.
  • Test both directions on every connectivity fault. One-way reachability is the signature of a Layer 2 mapping problem, and a ping run from one end cannot see it. Here the working direction was read as evidence of health for an hour.
  • Keep the two questions apart in habit and in runbooks. Whether the router chose the right next hop is show ip route; whether it can address a frame to that next hop is ip neigh. Only the second one is about this fault, and only the first one was run.