How do I limit simultaneous connections to a port with UFW?

I see many articles regarding brute attacks, number of connections per N seconds, but in my case is different, i have a proxy server and i just want to limit connections from anywhere to 1 at the time... again, not from the same IP, but from anywhere... i tried using ufw limit but that is for connections coming from the same IP Address, i just want that, if somebody is currently connected to port 3128 then nobody else can connect until the current connection is finished.


Solution 1:

UFW is using iptables as backend. UFW's limit rule is based on iptables' recent match which is not intended for OP's use case. The relevant feature would instead be the connlimit match (and for nftables the ct count expression).

connlimit

Allows you to restrict the number of parallel connections to a server per client IP address (or client address block).

Alas, this feature is not made available directly by UFW which is called the Uncomplicated Firewall for a reason. Instead, to provide missing functionalities, UFW offers to load any custom iptables rules from files (as long as it's not in user.rules/user6.rules). So this answer will actually be an iptables answer hidden inside an UFW answer.

The standalone iptables rule to limit the concurrent number of connections on tcp port 3128 to one could be written like this:

iptables -A INPUT -p tcp --dport 3128 -m conntrack --ctstate NEW -m connlimit --connlimit-upto 1 --connlimit-mask 0 --connlimit-saddr -j ACCEPT

-m conntrack --ctstate NEW verifies only new connections, --connlimit-mask 0 --connlimit-saddr will match any source address, and --connlimit-upto 1 will allow only one conntrack entry matching the conditions for the result to be true.

This rule should be put, in iptables-save format (just meaning there's no leading iptables command), in the dedicated custom chain called ufw-after-input defined in the /etc/ufw/after.rules. So it will use -A ufw-after-input instead of -A INPUT.

The end of /etc/ufw/after.rules should now read like this (at least for UFW 0.36):

# don't log noisy broadcast
-A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input

# allow only one established connection to port 3128
-A ufw-after-input -p tcp --dport 3128 -m conntrack --ctstate NEW -m connlimit --connlimit-upto 1 --connlimit-mask 0 --connlimit-saddr -j ACCEPT

# don't delete the 'COMMIT' line or these rules won't be processed
COMMIT

and the firewall rules reloaded:

ufw reload

Notes:

  • Local connections won't be affected, and the rule can't be made to work correctly with local connections for multiple reasons. Verifying this rule must be done from remote.

  • IPv6: One can insert the same entry (starting this time with -A ufw6-after-input) in the same way in /etc/ufw/after6.rules. Note that iptables and ip6tables won't coalesce the two counts together: this will then allow one IPv4 connection plus one IPv6 connection.

  • While examples often use --syn, using -m conntrack --ctstate NEW allows to replace the TCP protocol with UDP just by using -p udp instead of -p tcp, should the need arise.

  • Abnormal disconnections from the single allowed client (eg: loss of connectivity) might result in the socket being in FIN_WAIT states and would incur delays before being usable again. Also a browser using a proxy is likely to attempt multiple simultaneous connections through it. Allowing only one connection might disrupt the browser's behaviour with the default deny/DROP rule. Allowing more than one simultaneous connection, using an inverted iptables rule ending with -j REJECT or a ufw default reject incoming policy should be considered.