1

I have a unique situation in which a DDOS attack for a certain game is sending realistic player connection packets that seem to perfectly mimic a real player's connection packet.

These typically cycle through what looks to be thousands of IPs, sending one request and filling up connection slots on the server, preventing any real player from being able to connect.

Once connected, a real player begins sending multiple UDP packets at a high frequency, but these fake packets only send once per IP.

Is there a way I can drop the first received packet from an IP, but then allow all further packets?

Server is running on Debian Linux, 9.x.

1 Answers1

2

iptables + ipset

You could use the recent match extension, but for your use case it might be too limited for at least two reasons: the default module settings allow only for 100 entries which would limit to < 100 players without more tweaking and the expiration of stale entries is not automatic (it has to be done either from the packet path, which is not good performance-wise, or from some external batch job).

You can also use ipset, intended to be used along with iptables, whose features are a superset of recent and works better. It handles automatically removal of timed out entries.

So for example if this were to be applied on port 27960/udp...

ipset create firstseenpacket hash:ip timeout 60

iptables -N dropfirstseenpacket

iptables -I INPUT -p udp -m udp --dport 27960 -m set ! --match-set firstseenpacket src -j dropfirstseenpacket
iptables -I INPUT 2 -p udp -m udp --dport 27960 -j SET --exist --add-set firstseenpacket src
iptables -A dropfirstseenpacket -j SET --add-set firstseenpacket src
iptables -A dropfirstseenpacket -j DROP

The first time a source is seen, the test will not find an entry and being inverted will jump to dropfirstseenpacket, which will add the entry and drop the packet. Further occurences from the same source will find the entry, refresh its timer (--exist) and will do nothing else. The rules could be further factorized with one more chain eg if the first test is more costly than just checking 27960/udp.

You can see the ipset contents with:

ipset save firstseenpacket

Please note I used a -I (insert) statement to be sure this is handled without interference. Putting it after an entry like -A INPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT should stay compatible and even improve performances and resources (by handing over responsibility of active entries from ipset to conntrack), since conntrack maintains its own set of all active flows anyway whenever it's activated. This would give something similar to:

iptables -A INPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT
iptables -A INPUT -p udp -m udp --dport 27960 -m set ! --match-set firstseenpacket src -j dropfirstseenpacket
iptables -A dropfirstseenpacket -j SET --add-set firstseenpacket src
iptables -A dropfirstseenpacket -j DROP
iptables -A INPUT -p udp -m udp --dport 27960 -m conntrack --ctstate NEW -j ACCEPT

The activity timeout after 2nd packet now shifts to conntrack's handling if a reply is sent, with a timeout being 30s or 180s (or 120s on latest kernels) depending on various conditions.

You should verify if it's fine with your specific rules.

nftables

Since it might become the successor to iptables, here's the equivalent to the first version with nft commands (and made to handle IPv4 and IPv6). Nftables handles natively sets. A recent enough nftables (>= 0.8.4 ?) must be used for the set statement feature allowing to add from packet path, so on Debian 9 it would require nftables from stretch-backports.

Boilerplate:

nft add table inet filter
nft add chain inet filter input '{ type filter hook input priority 0; }'

SET STATEMENT

The set statement is used to dynamically add or update elements in a set from the packet path. The set setname must already exist in the given table. Furthermore, any set that will be dynamically updated from the nftables ruleset must specify both a maximum set size (to prevent memory exhaustion) and a timeout (so that number of entries in set will not grow indefinitely). The set statement can be used to e.g. create dynamic blacklists.

Named sets for the equivalent feature to ipset:

nft add set inet filter ipv4firstseenpacket '{ type ipv4_addr; timeout 60s; size 5000; }'
nft add set inet filter ipv6firstseenpacket '{ type ipv6_addr; timeout 60s; size 5000; }'

Filter rules:

nft add rule inet filter input udp dport 27960 ip  saddr != @ipv4firstseenpacket add @ipv4firstseenpacket '{ ip  saddr }' drop
nft add rule inet filter input udp dport 27960 update @ipv4firstseenpacket '{ ip  saddr }'

nft add rule inet filter input udp dport 27960 ip6 saddr != @ipv6firstseenpacket add @ipv6firstseenpacket '{ ip6 saddr }' drop
nft add rule inet filter input udp dport 27960 update @ipv6firstseenpacket '{ ip6 saddr }' drop

Here in one shot, the entry is checked, if not found added and the packet dropped. There still must be an other action to refresh the timer. The ipv6 version is working the same.

You can see the set contents for example with:

nft list set inet filter ipv4firstseenpacket

To be thorough, with conntrack this would be instead (and could also be factorized with an additional chain):

nft add rule inet filter input ct state established accept
nft add rule inet filter input udp dport 27960 ip  saddr != @ipv4firstseenpacket add @ipv4firstseenpacket '{ ip  saddr }' drop
nft add rule inet filter input udp dport 27960 ip6 saddr != @ipv6firstseenpacket add @ipv6firstseenpacket '{ ip6 saddr }' drop
nft add rule inet filter input udp dport 27960 ct state new accept
A.B
  • 9,037
  • 2
  • 19
  • 37