Skip to main content
RunBook Academy

← All break/fix scenarios in VyOS

advancedvyos-ipsec~30 min

IPsec Tunnel Established but Routing Fails

Reported symptoms

  • Hosts on the new VLAN 140 subnet, 10.30.0.0/16, cannot reach anything at the branch site. Every destination fails identically and immediately.
  • Hosts on the original HQ subnet, 10.10.0.0/16, reach the branch normally and have done so throughout. The branch site reports no problem at all.
  • The VPN dashboard is green. show vpn ike sa and show vpn ipsec sa both report the peer established since the Tuesday change window, with bytes counting up in both directions.
  • The BGP session that was supposed to come up over the new tunnel interface has been in Active since the same window. It was recorded as a follow-up item rather than as a symptom.
  • ping 10.100.100.2 from the router returns "connect: Network is unreachable" instantly, rather than timing out the way an unresponsive peer would.
  • A firewall counter nobody watches, on the rule that drops RFC 1918 destinations heading for the WAN, started advancing at 21:16 on Tuesday and has not stopped.
  • The change ticket for Tuesday reads "migrate BRANCH-01 to route-based IPsec so new subnets are a route rather than a renegotiation" and was closed as successful at 21:20.

Evidence

  • · show configuration commands | match "site-to-site peer BRANCH-01" lists local-address, remote-address, authentication, the IKE and ESP groups, and tunnel 0 local prefix 10.10.0.0/16 with tunnel 0 remote prefix 10.20.0.0/16. There is no vti bind line.
  • · show configuration commands | match "interfaces vti" lists vti0 with address 10.100.100.1/30, mtu 1400 and an MSS clamp. The interface exists and is fully configured.
  • · show interfaces vti reports vti0 in state u/D - administratively up, link down - with no traffic counted in either direction since it was created.
  • · sudo swanctl --list-sas shows one child SA, INSTALLED, with local 10.10.0.0/16 and remote 10.20.0.0/16 as its traffic selectors. It is not the wildcard selector pair a bound VTI negotiates.
  • · sudo ip xfrm policy lists out and in policies for the two LAN prefixes only. No policy carries a mark, and no policy references vti0.
  • · show ip route 10.20.0.0/16 reports the static route via 10.100.100.2 as inactive. It is in the RIB and it is not in the FIB.
  • · ip route get 10.20.0.5 returns via 203.0.113.1 dev eth0 - the default route out of the WAN, not the tunnel.
  • · sudo tcpdump -ni eth0 proto esp shows ESP flowing steadily in both directions, which is the traffic from 10.10.0.0/16 that has never stopped working.
  • · show firewall ipv4 forward filter shows rule 90, described as drop private destinations toward WAN, counting since 21:16 Tuesday.
Diagnosis and resolutionclick to reveal

Root cause

The peer was never bound to the tunnel interface. On VyOS 1.4 and 1.5, vti bind is the statement that joins an IPsec connection to a VTI: it is what makes the connection negotiate the wildcard, mark-based traffic selectors a route-based tunnel uses, and it is what causes the interface to be brought up when the security association installs. The Tuesday change created vti0, addressed it, sized its MTU, clamped its MSS and added static routes over it, and left that one line out. Everything else about the peer is unchanged, so the connection is still exactly what it was before the window: a policy-based tunnel whose selectors are 10.10.0.0/16 and 10.20.0.0/16. That is why every health check passes. The tunnel is genuinely up and genuinely carrying traffic, for the prefixes its selectors describe, through the kernel policy database rather than through any interface. Meanwhile nothing brings vti0 up, so its connected route never appears, so the next hop 10.100.100.2 never resolves, so the static routes over it stay inactive in the RIB and never reach the forwarding table. Traffic from the new subnet matches no IPsec policy and finds no tunnel route, falls through to the default route, and is discarded at the WAN edge by the rule that drops private destinations. Three independent things have to agree for a route-based tunnel to carry traffic - the daemon's selectors, the interface state, and the forwarding table - and "the tunnel is up" is a statement about the first of them only.

Remediation

The repair is to finish the migration rather than to add anything further around it: set vpn ipsec site-to-site peer BRANCH-01 vti bind vti0, and delete vpn ipsec site-to-site peer BRANCH-01 tunnel 0. Route-based and policy-based are two descriptions of the same connection, not two features that combine; a peer carrying both is a policy-based tunnel with a decorative interface beside it. Route-based deployments also need set vpn ipsec options disable-route-autoinstall, which is global and removes the routes that any other policy-based peer on this router silently depends on, so check show vpn ipsec connections before touching it. The cost of the fix is the reason it does not happen at 09:30 on Wednesday: binding the VTI changes the child SA's traffic selectors, which forces a renegotiation, and the branch end must already be route-based or the negotiation fails and the 10.10.0.0/16 traffic that works today stops working too. That trades a broken new subnet for a broken production one. Hold is the correct first decision here - the new subnet has been down since Tuesday and can stay down until a window with the branch on the call, with a named owner and an end time - and the change itself belongs behind commit-confirm, because the session diagnosing this may be riding the tunnel it is about to renegotiate.

Verification

Verify on the selectors, not on the tunnel state, because the tunnel was up throughout the fault and will be up afterwards either way. sudo swanctl --list-sas must show the child SA with the wildcard selector pair rather than the two LAN prefixes; that single line is the difference between a bound VTI and a policy-based tunnel that has merely come back. Then walk the chain the fault broke: show interfaces vti vti0 shows u/u rather than u/D; the connected route for 10.100.100.0/30 appears; show ip route 10.20.0.0/16 shows the static route selected via 10.100.100.2 on vti0 with no inactive marker; ip route get 10.20.0.5 agrees; ping 10.100.100.2 answers. Test from both subnets, not only the one that was reported broken, because the change moved the mechanism that carries the working one. Prove the MTU with a do-not-fragment ping sized to the 1400-byte VTI rather than a default-size ping, since a tunnel that pings and cannot carry a file is the classic next incident. Confirm firewall rule 90 has stopped counting, and confirm the BGP session over the VTI reaches Established rather than leaving it as a follow-up item for a second time.

Prevention

Migrating a tunnel from policy-based to route-based means changing how the connection describes itself, not adding an interface next to it. Write the migration as one change that adds vti bind and removes the tunnel prefixes in the same commit, review it as a peer rather than as a diff, and treat the presence of both forms on one peer as a defect the review must catch. Verify a VTI migration by reading the negotiated selectors, because every other indicator - IKE state, child SA state, byte counters, the dashboard - reports healthy for the tunnel you did not intend to keep. Make a down VTI visible: an interface in u/D and a static route sitting inactive are silent by nature, and both are cheap to alert on. The same applies to the routes themselves, which are worth asserting in the forwarding table rather than in the configuration. Keep the egress rule that drops RFC 1918 destinations toward the WAN - it turned what would otherwise have been inner packets leaving in the clear into a counter that dated the fault to the minute. Finally, a two-ended change needs both ends on the call: the half of this migration that was done alone is the half that produced a tunnel nobody could tell was wrong.

Reported symptoms

hq-edge-01 is a VyOS 1.5 LTS router with eth0 on the WAN at 203.0.113.2/30 and the HQ core on eth1. It terminates one site-to-site IPsec tunnel, to BRANCH-01 at 198.51.100.10, which has carried traffic between the HQ subnet 10.10.0.0/16 and the branch subnet 10.20.0.0/16 for three years without an incident.

A new subnet, 10.30.0.0/16 on VLAN 140, was built the previous week for a team that needs an application at the branch. Adding a second subnet to a policy-based tunnel means renegotiating traffic selectors with the far end, so the Tuesday change window was booked to migrate the peer to route-based IPsec first. The ticket was closed successful at 21:20.

  • Wednesday 08:50, the VLAN 140 team. Nothing at the branch is reachable from the new subnet. Not the application, not a ping, not a name. Every attempt fails the same way and fails instantly.
  • Wednesday 09:05, the branch. Nothing is wrong at their end. Traffic between the branch and 10.10.0.0/16 has been normal all week, in both directions, including this morning.
  • Throughout. The VPN dashboard is green. show vpn ike sa and show vpn ipsec sa report the peer established since Tuesday 21:14 with bytes climbing in both directions.
  • Since Tuesday 21:14. The BGP session that was supposed to come up over the new tunnel interface has been in Active. It was written down as a follow-up rather than as a symptom, on the reasoning that BGP could be sorted out once the migration settled.

Everything visible says the tunnel is healthy, and the one subnet that anybody is watching agrees. The natural conclusion is that the new VLAN is the problem — a firewall rule, a missing route at the branch, a subnet nobody advertised — and the first hour goes there.

Evidence provided

The peer, as it stands after the migration:

show configuration commands | match "site-to-site peer BRANCH-01"

set vpn ipsec site-to-site peer BRANCH-01 authentication mode 'pre-shared-secret'
set vpn ipsec site-to-site peer BRANCH-01 authentication local-id '203.0.113.2'
set vpn ipsec site-to-site peer BRANCH-01 authentication remote-id '198.51.100.10'
set vpn ipsec site-to-site peer BRANCH-01 connection-type 'initiate'
set vpn ipsec site-to-site peer BRANCH-01 default-esp-group 'ESP-BRANCH'
set vpn ipsec site-to-site peer BRANCH-01 ike-group 'IKE-BRANCH'
set vpn ipsec site-to-site peer BRANCH-01 local-address '203.0.113.2'
set vpn ipsec site-to-site peer BRANCH-01 remote-address '198.51.100.10'
set vpn ipsec site-to-site peer BRANCH-01 tunnel 0 local prefix '10.10.0.0/16'
set vpn ipsec site-to-site peer BRANCH-01 tunnel 0 remote prefix '10.20.0.0/16'

The interface and the routes the change added:

show configuration commands | match "interfaces vti"

set interfaces vti vti0 address '10.100.100.1/30'
set interfaces vti vti0 description 'IPsec to Branch 01'
set interfaces vti vti0 ip adjust-mss clamp-mss-to-pmtu
set interfaces vti vti0 mtu '1400'

show configuration commands | match "static route 10.20"

set protocols static route 10.20.0.0/16 next-hop '10.100.100.2'
Read-only / Safethe selectors are the two LAN prefixes
$ sudo swanctl --list-sas
BRANCH-01: #4, ESTABLISHED, IKEv2, 8a1f4c2e9b0d5731_i 3c77e0a1d4b62f98_r
local  '203.0.113.2' @ 203.0.113.2[500]
remote '198.51.100.10' @ 198.51.100.10[500]
AES_CBC-256/HMAC_SHA2_256_128/PRF_HMAC_SHA2_256/MODP_2048
established 43920s ago, rekeying in 26880s
BRANCH-01: #9, reqid 1, INSTALLED, TUNNEL, ESP:AES_CBC-256/HMAC_SHA2_256_128
  installed 43920s ago, rekeying in 2280s, expires in 3600s
  in  c1e4a9f2, 918273645 bytes, 1204831 packets, 1s ago
  out 7fd10b3c, 402817364 bytes,  998214 packets, 1s ago
  local  10.10.0.0/16
  remote 10.20.0.0/16

Illustrative output

Read-only / Safeadministratively up, link down
$ show interfaces vti
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface    IP Address         S/L  Description
---------    ----------         ---  -----------
vti0         10.100.100.1/30    u/D  IPsec to Branch 01

Illustrative output

Read-only / Safein the RIB, not in the FIB
$ show ip route 10.20.0.0/16
Routing entry for 10.20.0.0/16
Known via "static", distance 1, metric 0
  10.100.100.2 inactive

Illustrative output

ip route get 10.20.0.5 answers via 203.0.113.1 dev eth0 — the default route. ping 10.100.100.2 does not time out; it returns connect: Network is unreachable immediately, because there is no route to the VTI subnet at all. And sudo tcpdump -ni eth0 'proto esp' shows ESP moving steadily in both directions, which is the 10.10.0.0/16 traffic that has never stopped.

One more counter, from a rule added during a hardening review two years ago and looked at by nobody since:

Read-only / Safecounting since Tuesday 21:16
$ show firewall ipv4 forward filter
Rule     Action   Protocol  Packets     Bytes       Description
90       drop     all       2418773     1834229104  drop private destinations toward WAN

Illustrative output

Work the evidence before reading on

Everything needed is in the three configuration listings and the swanctl output. The tunnel is up; that fact is true and is not the answer.

  1. Read the child SA’s local and remote selectors. Compare them with what a tunnel bound to a VTI negotiates. What kind of tunnel is this, and does the configuration listing agree?
  2. vti0 is administratively up and link down. Ask what is supposed to bring a VTI up, and look for that thing in the peer listing.
  3. Follow the static route to 10.20.0.0/16 from the configuration to show ip route to ip route get. At which of those three does it stop existing, and what does the interface state have to do with it?
  4. 10.10.0.0/16 reaches the branch and 10.30.0.0/16 does not, on a router where neither subnet has a working route through the tunnel. By what mechanism does the first one get there?
  5. Rule 90 has counted 2.4 million packets since two minutes after the change. What is in those packets, and what would have happened to them on a router without that rule?

Before continuing: name the single configuration line that is missing, and predict what show vpn ipsec sa would have reported if the migration had been done correctly and the branch end had not been.

Root cause

1. vti bind is the migration, and it is not there

On VyOS 1.4 and 1.5 a route-based peer is declared with set vpn ipsec site-to-site peer BRANCH-01 vti bind vti0. That one line does two things that nothing else does. It makes the connection negotiate the wildcard, mark-based traffic selectors that a route-based tunnel uses, so the tunnel carries whatever routing sends into the interface rather than a fixed pair of prefixes. And it is the association that causes the interface to be brought up when the child SA installs, and taken down when it goes away.

The change window created the interface, addressed it, sized its MTU, clamped its MSS and added routes over it. It did not bind it. So the peer is unchanged from what it was on Monday: tunnel 0 local prefix and tunnel 0 remote prefix are still there, and they are still the whole definition of what this tunnel encrypts.

2. A policy-based tunnel does not need a route, which is why it kept working

Policy-based IPsec matches in the kernel’s policy database, not in the routing table. A packet from 10.10.0.0/16 to 10.20.0.0/16 matches the out policy ip xfrm policy lists, is encrypted, and leaves as ESP — regardless of what the routing table thinks the path to 10.20.0.0/16 is. That is why the original subnet has been fine throughout, why the byte counters climb, and why every check anyone ran reported a healthy tunnel. It is healthy. It is carrying the traffic its selectors describe, by the mechanism it has always used.

Traffic from 10.30.0.0/16 matches no policy, because no selector mentions it. It gets no encryption and it finds no tunnel route, so it follows the default route toward the WAN with an RFC 1918 destination still on it, and rule 90 drops it at the edge. Those 2.4 million packets are the new subnet’s entire attempt to reach the branch since 21:16 on Tuesday.

3. An unbound VTI is down, and a next hop on a down interface does not resolve

Nothing ever brings vti0 up, because the thing that would have is the binding. The interface sits in u/D: configured, addressed, and without carrier.

An interface with no carrier contributes no connected route, so 10.100.100.0/30 is not in the routing table, so 10.100.100.2 — the next hop of every static route the migration added — cannot be resolved. FRR keeps the route in the RIB and marks it inactive rather than installing it. This is the part that makes the fault feel impossible: the routes are in show configuration, they are in show ip route, and they are not in the forwarding table, and only the third of those decides where a packet goes.

Resolution

  1. Establish what the far end is running before proposing anything. A route-based branch means one short window; a policy-based branch means the migration is half done and both halves belong in the same change.
  2. Record the hold explicitly. The new subnet has been unable to reach the branch since Tuesday and can remain so until a window; the production subnet cannot. Name the owner, name the end time, and put both in the ticket rather than letting "we will do it when we can" stand in for a decision.
  3. Check for other peers before touching global options. show vpn ipsec connections and show configuration commands | match "vpn ipsec (interface|options)"disable-route-autoinstall is required for route-based and removes routing that any policy-based peer on this router may depend on.
  4. Write the change as one commit that binds the VTI and removes the policy-based selectors together: set vpn ipsec site-to-site peer BRANCH-01 vti bind vti0 and delete vpn ipsec site-to-site peer BRANCH-01 tunnel 0. Leaving both forms present is what produced a tunnel that read route-based and behaved policy-based.
  5. Review with compare and read the peer as a whole, not as a diff. The question to ask of the result is which single mode it now describes.
  6. Apply with commit-confirm 10. The session running this change may be riding the tunnel it renegotiates; the timer is the only thing that returns the router if the far end does not come back.
  7. Watch the renegotiation rather than assuming it: sudo swanctl --list-sas for the new selectors, show log ipsec or journalctl -u strongswan if it does not install. A failure here is a far-end mode mismatch and is diagnosed from the log line, not by retyping local parameters.
  8. Once the SA is installed, confirm vti0 has come up and the connected route has appeared before looking at the static routes. The routes cannot install until it does.
  9. confirm to cancel the timer, then save. An unconfirmed commit reverts and an unsaved commit survives only until the next reboot.

Verification

  1. The selectors changed. sudo swanctl --list-sas shows the child SA with the wildcard selector pair rather than local 10.10.0.0/16 and remote 10.20.0.0/16. This is the check that distinguishes a repaired binding from a tunnel that has merely come back up, and it is the one the original migration never ran.
  2. The interface is up. show interfaces vti vti0 shows u/u, and the connected route for 10.100.100.0/30 is present in show ip route.
  3. The routes are installed, not merely configured. show ip route 10.20.0.0/16 shows the static route selected via 10.100.100.2 on vti0 with no inactive marker, and ip route get 10.20.0.5 agrees.
  4. The tunnel transit works: ping 10.100.100.2 answers. During the fault this returned Network is unreachable, so it is a clean before-and-after.
  5. Both subnets reach the branch, tested from a host in each. The change moved the mechanism carrying 10.10.0.0/16 from the policy database to the interface, so the subnet that was working is the one most likely to have been disturbed.
  6. The MTU holds under load, not only under ping. ping -M do -s 1372 -c 3 10.20.0.5 fills the 1400-byte VTI; then confirm a default-size ping still works, so a failure above reads as too big rather than no path.
  7. Firewall rule 90 has stopped counting. Its counter was the only thing in the estate that saw this fault, and it should now be flat.
  8. The BGP session over the VTI reaches Established. It was a symptom before the fix and is a verification after it; leaving it as a follow-up a second time repeats the mistake that hid the fault.

Prevention

  • Migrate the connection, do not decorate it. Route-based and policy-based are two descriptions of one tunnel. A peer carrying vti bind and tunnel 0 prefixes at the same time is not a hybrid; it is a policy-based tunnel with an unused interface beside it, and the review that reads the diff line by line will not notice.
  • Verify a VTI migration on the negotiated selectors. Every other indicator — IKE state, child SA state, byte counters, the dashboard — reports healthy for the tunnel you meant to stop using. The selectors are the only output that distinguishes the two modes.
  • Alert on a VTI in u/D and on a static route that is inactive. Both states are silent by construction, both are cheap to query, and between them they describe every way a route-based tunnel can be configured and carry nothing.
  • Assert the route in the forwarding table, not in the configuration. show configuration proved nothing here for fourteen hours. ip route get would have settled it in one command.
  • Keep the egress rule that drops RFC 1918 destinations toward the WAN. It converted what would otherwise have been inner packets leaving in the clear into a counter that dated the fault to the minute.
  • Put both ends of a two-ended change on the same call. Half a tunnel migration is not half working. It is a tunnel that reports healthy in the old mode while everything built for the new mode sits inert.