You run tcpdump on the outside interface of a Linux IPsec gateway and see two things at once: inbound ESP frames and, right after them, the decrypted inner packets. Outbound, only ESP appears. The natural conclusion is that replies are not being sent, or that IPsec encrypts one direction and forgets the other. Neither is true.

What you are looking at is a capture point. The kernel transforms outbound packets before the device's transmit path hands them to the packet socket, so the socket never sees the inner form going out. Inbound, the packet is captured as ESP first and the decapsulated copy is visible when it re-enters the stack. Same traffic, two different observation windows.

The capture point decides what you see

On a normal interface, an AF_PACKET socket sits near the device layer. Packets leaving the host pass through IPsec output processing first, which turns an inner TCP segment into an ESP or UDP-4500 datagram, and only then reach that layer. Packets arriving are copied to the socket as they come off the wire, still encrypted, and the decrypted result becomes visible later when the stack delivers it locally or forwards it.

That ordering explains the asymmetry completely. It is not evidence about direction, policy, or application behavior, and treating it as such sends people chasing a reply that was sent correctly all along. It is one of the few networking puzzles where the instrument, not the network, creates the anomaly.

Match the filter to the layer you mean

An expression like tcp port 443 selects an inner TCP connection. Outbound, that connection has already become ESP by the time the socket is consulted, so the filter matches nothing. If you want to count protected traffic, filter on the outer form instead: protocol 50 for raw ESP, or UDP port 4500 when NAT traversal is in play. Both filters produce a packet count that can be compared between the two peers, which is usually the reason for counting at all. Neither of them tells you whether the inner request was answered.

Do not treat a UDP 4500 count as an ESP packet count. IKE also uses that port for NAT-T keepalives and for the encapsulated exchange, so the number includes traffic that is not the data you meant to measure. The initial IKE exchange on UDP 500 needs its own filter if that is the question. A number that mixes control traffic with data also makes any bandwidth estimate wrong by an unknown margin. Separate counts answer separate questions.

Write the filter down next to the question it answers. "How much ESP left the box" and "did the web request leave the box" are different measurements with different filters, and one capture cannot settle both. Without that note, two people will compare numbers that were never counting the same thing.

Watch before the transform, not after

If you need to see outbound plaintext, move the observation point earlier, before the IPsec output processing. NFLOG gives you a Netfilter hook where a rule can copy selected packets to a userspace socket, which tcpdump can read. strongSwan's traffic-dump guide documents this approach for exactly this reason.

The policy match limits the rule to traffic that is about to enter a tunnel. -m policy --pol ipsec --dir out is what makes the rule specific instead of logging every packet the box sends. Then read the log group with a bounded capture: a small snapshot count and a size limit keep the diagnostic from becoming a traffic archive. A bounded capture is also easier to hand to a reviewer without dragging unrelated payload along with it. Decide the count before you start, not after you see how much arrived.

iptables -I OUTPUT 1 -d 10.83.0.44/32 -p tcp --dport 8443 -m policy --pol ipsec --dir out -j NFLOG --nflog-group 5
tcpdump -ni nflog:5 -s 128 -c 20 'host 10.83.0.44 and tcp port 8443'

Choose the hook to match the flow. OUTPUT covers traffic the gateway generates itself; transit from a LAN client passes the FORWARD chain instead, and a rule on OUTPUT will never see it. The address and port above are placeholders for an approved test destination.

What the observation proves

NFLOG is a logging target, not an acceptance decision. A packet appearing there shows that it reached the hook and matched the rule. It does not show that a later filter allowed it, that the device transmitted it, or that the peer received anything.

An empty log is equally ambiguous. Check the rule's packet counter, confirm the direction, confirm that the flow actually matched an IPsec policy, and confirm the destination. Only a matching counter on both peers turns an observation into a conclusion. Then compare the security association counters on both peers for the same controlled attempt, because the SA counters are the authoritative record of what was protected and sent.

One more trap: "plaintext" in this context means outside the IPsec encryption layer. An inner HTTPS session still has its own TLS. Capturing the decapsulated packet does not decrypt the application's TLS, and seeing readable HTTP there means the application was never using HTTPS in the first place.

Clean up and compare both ends

Remove the exact rule you inserted, by its owner, as soon as the observation is done. Leaving a logging rule in place costs CPU on every matching packet and quietly creates a data retention problem. Delete by repeating the rule with -D rather than flushing a chain you do not own.

iptables -D OUTPUT -d 10.83.0.44/32 -p tcp --dport 8443 -m policy --pol ipsec --dir out -j NFLOG --nflog-group 5

If the deployment uses user-space IPsec, an XFRM interface, or hardware offload, the observation points move and this recipe may not apply at all. With libipsec the documented place to watch inner traffic is the TUN device. Identify the path first, then pick the point that answers the question, and expect the two directions to look different only when you are looking at the wrong side of the transform.

Nuŋlɔɖi siwo ku ɖe eŋu