bk99.de entertain the web since 1997

SYN-ACK reflection: when your own web server works for someone else’s attack

Summary

In October 2026 the firewall log of my home network suddenly showed hundreds of connection attempts to my reverse proxy, all from two networks of a Polish provider. The analysis revealed crafted SYN packets whose replies nobody ever answered, so most likely not an attack on me but on the network of the supposed senders. This post shows how to recognise SYN-ACK reflection, why my country block did not apply and how I drop the packets in OPNsense for a limited time.

Many hits, but not a single request

My home network sits behind an OPNsense firewall, where Caddy runs as a reverse proxy for a few self-hosted services. The pass rule for Caddy logs every new connection. Normally there are only a few entries per hour.

One evening there were suddenly 361 entries in just under half an hour. They came from 313 different addresses, all from the same /21 network, plus a few from a /23 of the same provider. Each address appeared only once to three times, at a steady 8 to 20 packets per minute.

That looked like a distributed attack. Caddy itself, however, never saw a single one of these connections. The reason: the firewall logs the very first packet of a connection attempt. Whether a connection is ever established is not in the log.

Takeaway: A “pass” in the firewall log is a connection attempt, not an access. Before assuming an attack on your application, check whether a connection was established at all.

What the packets reveal

A TCP handshake has three steps: the client sends SYN, the server answers with SYN-ACK, the client confirms with ACK. The firewall state table shows how far each attempt got.

160 of 190 open connections from these networks looked the same: one packet in, four packets out, meaning the SYN-ACK and three retransmissions. The rest differed only by a repeated SYN. No answer to the SYN-ACK came back in any case, neither ACK nor RST. According to RFC 4987, a real, reachable host answers an unexpected SYN-ACK with RST.

The packets themselves were unusual too. Every SYN was 44 bytes long. A Linux host typically sends 60 bytes because it includes options such as timestamps, window scaling and SACK. On top of that, TTL values ranged from 64 to 185 within one and the same network. An operating system starts with a fixed value, usually 64, 128 or 255, and the hop count inside one provider network is similar. Values spread this widely point to packets assembled one by one by a program.

Takeaway: For suspicious traffic, look at the state table, not just the log. Packet size, TTL and whether an ACK ever arrives separate a real client from crafted packets.

Reflection, not an attack on me

The pattern fits SYN-ACK reflection. The attacker sends SYN packets to many servers on the internet and puts addresses from the real target’s network in as the sender. Every server dutifully replies with SYN-ACK and retransmissions, to the victim. My Caddy was one of probably very many unwitting helpers.

The senders being spread across a whole /21 with each address appearing rarely fits as well. This spreading is often called “carpet bombing”: protection systems that look for individual heavily loaded destination addresses react late or not at all.

I cannot prove this from my side. A scanner that suppresses its own replies would also be conceivable. The widely spread TTL values and the complete silence from more than 300 addresses argue against it.

The risk for me was low. No connection was established, the application was never reached, and about 3,200 of almost 792,000 possible state table entries were in use.

Takeaway: Not every attack you see is aimed at you. Anyone exposing services to the internet can act as an amplifier for other people’s attacks.

Why the country block did not apply

On my firewall a floating rule at the very top drops all packets from a few countries. It is based on a GeoIP alias filled from MaxMind’s free GeoLite2 database. A log viewer showed the senders as Russian, so at first I wondered why the rule did not apply.

The answer was simple: the firewall worked correctly. According to the RIPE registration and to MaxMind, both networks belong to a provider in Poland, and Poland is not on my list. Different geo databases place the same address differently.

I also made a measurement error. The list query of the OPNsense API returned only the first 9,999 entries of the alias, sorted by address. For a moment I therefore thought the alias was incomplete and IPv4 only. In fact it holds more than 58,000 networks, almost half of them IPv6. Only a search across the full table showed that.

Takeaway: Check the origin of an address with the same database your firewall uses, for example via RIPEstat. And if a list has about 9,999 or 10,000 entries, it is probably truncated.

Countermeasure: drop for a limited time

I can do little against the attack on the victim. But I can stop taking part in it. I created a separate alias with the two affected networks and added a floating rule directly after the country block. It drops incoming packets from these networks on both internet uplinks before the pass rule for Caddy applies.

It matters to use “block” with drop, not “reject”. A reject sends the sender an RST or an ICMP message, and those end up at the victim again. In pf syntax the rule looks roughly like this, here with example networks from RFC 5737:

table <reflexion> { 198.51.100.0/24 203.0.113.0/24 }
block drop in log quick on $wan inet from <reflexion> to any

Right after applying it, the effect shows in the log: new SYN packets from these networks appear as “block”. In my case there were nine in the first minute, all dropped. State entries from before expire on their own.

The block deliberately lasts only one month. Behind the addresses are real customers of a provider who have nothing to do with the attack. A permanent block would lock them out for no reason. Removing it is in my task list with a date.

Takeaway: Implement blocks against reflection as drops, limit them tightly to the affected networks and give them an expiry date.

The actual root cause

Such an attack is only possible because somewhere on the internet a network lets packets with forged sender addresses out. BCP 38, published as RFC 2827, has described the countermeasure since 2000: providers only let packets leave their customer networks if the sender address belongs to those networks.

As long as this is not deployed everywhere, reflection remains a cheap attack. The attacker needs little bandwidth and hides behind other people’s servers.

Takeaway: If you run your own network with a public address range, only allow your own sender addresses outbound. It costs one rule and protects everyone else.

Numbers at a glance

  • In just under 30 minutes, 361 SYN packets arrived from 313 addresses in a single /21 network.
  • 160 of 190 connections consisted of one packet in and four packets out; no ACK or RST came back in any case.
  • The SYN packets were 44 bytes long; a typical Linux SYN is 60 bytes.
  • The GeoIP alias held more than 58,000 networks; the API list query showed only the first 9,999.
  • BCP 38 (RFC 2827) has described filtering forged sender addresses at the provider since May 2000.

References

Remarks

  • Classifying this as reflection is a conclusion drawn from the packet pattern. I do not know the attacker or the actual target.
  • I deliberately do not name the affected networks: they most likely belong to the victims, not the attackers.

Read RFC 4987 on SYN flooding