A legitimate host stopped working on my LAN. It took me an afternoon, because the two logs you would normally check were both completely clean.
The symptom was that `curl https://client-update.steamstatic.com/…\` returned `000` with `OpenSSL SSL_connect: SSL_ERROR_SYSCALL`. Not refused, not a timeout, not a certificate error. The connection was being killed partway through the TLS handshake. Worth saying up front, because it cost me time: `SSL_ERROR_SYSCALL` means the socket died mid-handshake. It is not a TLS problem, so don't start on ciphers or certificates like I did.
Everything I checked came back clean. DNS resolved fine. Three days of pf filter logs had zero blocks for that destination, and the IP was in no pf table at all. Suricata had no alerts for the host and no reject rules enabled.
So I captured it on the WAN while reproducing it:
SYN ->
<- SYN-ACK
ACK ->
ClientHello len 517 ->
FIN seq 518 -> 78 microseconds later
<- ServerHello + cert, 4916 bytes
RST ->
The server was answering perfectly. The teardown came from my side, immediately after the SNI went on the wire.
The tell is the TCP options. Every genuine packet in the flow carried `options [nop,nop,TS val … ecr …]`. The FIN and the RST had no timestamp option and an unscaled window. Once timestamps are negotiated, a packet in that flow which drops them did not come from the endpoint's TCP stack. Something inline injected it. That signature identifies any injecting middlebox, not just this one.
To localise it, run the same request from three places. From a LAN client behind the firewall it fails. From the OPNsense box itself you get 200. From a host outside the LAN you get 200. Router-originated traffic is not forwarded, so it never crosses an inline engine, which means "works from the router, fails from behind it" points straight at the forwarding path. Confirming took another 30 seconds: `zenarmorctl engine stop` and it works, `start` and it breaks again.
As for why, my policy rejected 12 of 82 web categories, one being Firstly Seen Sites, a heuristic for not-yet-classified domains. `cdn.steamstatic.com` was fine while `client-update.steamstatic.com` was not, and the difference between them is basically how often anyone requests them. I could not confirm the category, since the categorisation databases are binary, so treat that as my best explanation rather than proven. That Zenarmor was doing the killing is not in doubt.
I removed it, and moved the filtering to layers whose blocks I can actually see. Seven of those twelve categories were already covered by my DNS blocklists (AdGuard plus Hagezi) or by Suricata's abuse.ch feeds, so they were duplicated work. The real gap was adult content, which no Hagezi list attempts, and OPNsense ships an OISD NSFW preset that covers it. I lost Illegal Drugs and Hate/Violence, which have no DNS equivalent. That was an acceptable trade for me. It might not be for you, and it is the one honest reason to keep the product.
The lesson is not "Zenarmor is bad". It is that running three filtering layers with two log surfaces means one of them fails silently. Know which layer logs where, because "pf is clean and Suricata is clean" does not mean the firewall is innocent.
Source: r/zenarmor · by /u/AntixMars