You brought the tunnel up, the rekey timer is counting down, and both directions still read zero packets. Nothing in that pair of numbers says the peer rejected your tunnel: an installed CHILD SA with idle counters is the normal state of an association that nothing has matched yet. The work is to prove that a packet should have matched, then find the point where it left the path the selectors describe. Do it once, in order, and keep the request identical between runs.

What a zero counter actually proves

A CHILD SA is a selector pair plus the keys and SPI that encrypt traffic falling inside those ranges. Installing one means only that both peers agreed on those ranges and derived keys; the kernel will hold it for hours without a single packet arriving. So the question is never whether the tunnel exists, but whether any packet your test sent fell inside the ranges the two peers negotiated. If none did, the counters stay at zero and the association expires unused at the end of its lifetime.

That distinction changes what you inspect first. A rejected proposal, an authentication failure or a dying IKE SA all surface in the log or in the SA list, not as a silent zero. A tunnel that negotiates cleanly and then stays idle points at matching, routing or the origin of the test traffic instead.

Send a flow you can describe before you send it

Pick a destination inside the protected subnet and open a new connection to it; a page you loaded an hour ago may be served from cache, and a cached reply never touches the tunnel. Then write down four values while the request is still hypothetical: the source address the application will bind, the destination, the address family, and the name or selector pair of the child you are watching. That record is what turns a flat counter into a testable claim.

Source address is the value people skip, and it is the one that breaks the test most often. A laptop with a wired address, a Wi-Fi address and a tunnel address can bind any of them depending on the route lookup, and only one may sit inside the negotiated range. Use a tool that lets you pin the source, such as ping -I 10.1.0.5 10.2.0.9 or curl --interface 10.1.0.5, so the packet cannot quietly pick a different address. When the pinned run moves the counters and the unpinned run does not, you have found a routing problem rather than an IPsec problem.

Which numbers to read, and where they live

On Linux, ip -s xfrm state prints each association with a lifetime current: line whose byte total and packet total belong to that SA alone, followed by a stats: line for replay and error counts. On a strongSwan peer, swanctl --list-sas prints one in line and one out line per installed child, each ending in a byte count and a packet count; the older stroke tooling labels the same pair bytes_i and bytes_o. Read the two directions separately, because the one that stayed flat is the finding. Adding them together hides exactly the asymmetry you are looking for.

Both views are per association, so a rekey installs a fresh SPI pair and your baseline returns to zero with nothing broken. Record the SPI you are watching before the rekey timer fires, or you will compare a new SA against an old number. If outbound rises while inbound stays flat, the selector matched and the reply never came back, which moves the investigation to the peer's forwarding and its own policy. strongSwan's IKEv2 configuration documentation covers the child SA lifecycle and the rekey timing behind those figures.

The observation point is a separate variable from the origin of the packet. A hardware offload can increment the association's counters while a software capture on the interface shows nothing, because offloaded packets skip the hook the capture sits on, and ingress can diverge the same way. When the two disagree, use the counters to answer whether anything matched and the capture to answer what the packet looked like.

Route, selectors and where the packet was born

Compare three things side by side: the route the kernel picks for your destination, the policy that steers traffic into the tunnel, and the selector pair the child negotiated. ip route get 10.2.0.9 from 10.1.0.5 names the interface and source the kernel would use, which is often not the pair you assumed. ip xfrm policy shows the templates and selectors installed locally, while swanctl --list-sas shows what the peer agreed to. A lookup that sends the packet out of the physical interface never reaches the policy at all, so the counters cannot move.

Mismatches come in several flavours that look identical from a counter. An IPv6 destination outside an IPv4 selector, a source address from the wrong subnet, and a policy narrower than the child all produce the same zero. Fix the narrowest cause first, because widening a selector just to shift a number changes what the tunnel protects.

Then there is the packet's origin, the trap a gateway sets for its own operator. Traffic the gateway generates itself is matched against output policy, while a packet it forwards for a client is matched against forward policy, so a ping from the box can succeed on a path no client ever takes.

One reversible change and a report that survives review

Change a single element, repeat the identical request and read the same counters. Clearing every firewall rule or replacing all selectors at once will move a number and destroy the evidence that told you why. If the second run still shows zero, you have eliminated one hypothesis instead of wasting the afternoon.

Write the finding in a form someone else can reproduce: child name, accepted ranges, the source you pinned, the destination you tested, the direction that stayed at zero, and the SPI you were reading. That sentence separates an association nobody used from a remote application that received the traffic and refused it. Roll the change back when it did not move the counters, and keep the record; the next person starts where you stopped. Nothing in the tunnel needed rebuilding for any of this, which is the point of checking matching before touching configuration.

Makomi ya ndenge moko