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.
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