Bug 45500 - Firewall: Rule order doesn't lead to predictable iptables rules
Firewall: Rule order doesn't lead to predictable iptables rules
Status: NEW
Product: UCS
Classification: Unclassified
Component: Firewall (univention-firewall)
UCS 5.0
Other Linux
: P5 normal (vote)
: ---
Assigned To: UCS maintainers
UCS maintainers
:
Depends on: 25632
Blocks:
  Show dependency treegraph
 
Reported: 2017-10-10 17:09 CEST by Mathieu Simon
Modified: 2022-05-04 07:25 CEST (History)
3 users (show)

See Also:
What kind of report is it?: Bug Report
What type of bug is this?: 3: Simply Wrong: The implementation doesn't match the docu
Who will be affected by this bug?: 1: Will affect a very few installed domains
How will those affected feel about the bug?: 3: A User would likely not purchase the product
User Pain: 0.051
Enterprise Customer affected?:
School Customer affected?:
ISV affected?:
Waiting Support:
Flags outvoted (downgraded) after PO Review:
Ticket number:
Bug group (optional):
Max CVSS v3 score:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Mathieu Simon 2017-10-10 17:09:46 CEST
Hi

I've found about this issue when I decided to drop all Packets to udp/161 (SNMP) but accept 2 IPs to poll the SNMP daemon.

The rule order is not predictable:
ucr set security/packetfilter/package/univention-snmpd/tcp/161/10.1.1.20='ACCEPT'
ucr set security/packetfilter/package/univention-snmpd/tcp/161/10.1.1.200='ACCEPT'
ucr set security/packetfilter/package/univention-snmpd/tcp/161/all='DROP'
systemctl restart univention-firewall

When looking at the resulting iptables rules this gives us:

# iptables-save | grep 161 | grep tcp
-A INPUT -d 10.1.1.20/32 -p tcp -m tcp --dport 161 -j ACCEPT
-A INPUT -d 10.1.1.200/32 -p tcp -m tcp --dport 161 -j ACCEPT
-A INPUT -p tcp -m tcp --dport 161 -j DROP

In this case 10.1.1.20 and .200 can reach snmpd.

However and since a typical snmpget or snmpwalk uses UDP defining eqivalent rules such as:

ucr set security/packetfilter/package/univention-snmpd/udp/161/10.1.1.20='ACCEPT'
ucr set security/packetfilter/package/univention-snmpd/udp/161/10.1.1.200='ACCEPT'
ucr set security/packetfilter/package/univention-snmpd/udp/161/all='DROP'
systemctl restart univention-firewall

... gives us:

# iptables-save | grep 161 | grep udp
-A INPUT -d 10.1.1.20/32 -p udp -m udp --dport 161 -j ACCEPT
-A INPUT -p udp -m udp --dport 161 -j ACCEPT
-A INPUT -d 10.1.1.200/32 -p udp -m udp --dport 161 -j ACCEPT

In that case, since iptables works on a first match basis, 10.1.1.200 cannot do a successful SNMP walk.

I could confirm that this behaviour is not limited to Net-SNMP when doing something similar with udp/123 (security/packetfilter/package/univention-base-files/udp/123)

# iptables-save | grep 123 | grep udp
-A INPUT -d 10.1.1.201/32 -p udp -m udp --dport 123 -j ACCEPT
-A INPUT -d 10.1.1.20/32 -p udp -m udp --dport 123 -j ACCEPT
-A INPUT -p udp -m udp --dport 123 -j DROP
-A INPUT -d 10.1.1.202/32 -p udp -m udp --dport 123 -j ACCEPT
-A INPUT -d 10.1.1.200/32 -p udp -m udp --dport 123 -j ACCEPT

And it neither is for tcp ports which I played then to pinpoint how things might work:

# iptables-save | grep 389 | grep tcp | grep -v 7389
-A INPUT -d 10.1.1.20/32 -p tcp -m tcp --dport 389 -j DROP
-A INPUT -p tcp -m tcp --dport 389 -j DROP
-A INPUT -d 10.1.1.203/32 -p tcp -m tcp --dport 389 -j DROP
-A INPUT -d 10.1.1.201/32 -p tcp -m tcp --dport 389 -j DROP
-A INPUT -d 10.1.1.200/32 -p tcp -m tcp --dport 389 -j DROP
-A INPUT -d 10.1.1.202/32 -p tcp -m tcp --dport 389 -j DROP

I think the current templating mechanism has some race conditions and might require some extension which might result:
* If a global all ACCEPT is defined, all specific DROPs on a port should be put before the global drop (otherwise the global ACCEPT will match)
* Similarly if an all DROP is defined but some IPs are are specifically allowed to reacht the port make sure the DROP gets put in at last.

Looking forward to hearing from you.

Mathieu
Comment 1 Florian Best univentionstaff 2017-10-10 17:30:37 CEST
I think an solution would be to add another UCR variable:
security/packetfilter/{tcp,udp}/.*/priority ?

Then the rules could be set in a specified order.
Comment 3 Mathieu Simon 2017-10-17 14:42:40 CEST
I'd welcome a more programmatic approach where some "intelligence" is added to the way the templated date gets filled in.

Priorities would be useful, but having at least a differenciation between global deny and accept rules would be much appreciated as it helps getting consistent results out of the box without thinking how it's going to be right.

-- Mathieu
Comment 4 Philipp Hahn univentionstaff 2022-05-04 07:25:41 CEST
(In reply to Mathieu Simon from comment #0)
> I've found about this issue when I decided to drop all Packets to udp/161
> (SNMP) but accept 2 IPs to poll the SNMP daemon.

The UCS firewall currently does not support this and you are hit here by Bug #25632: in the current implementation (UCS 4.4 and 5.0) the address is used as the *destination* address, not the *source* address:

> The rule order is not predictable:
> ucr set
> security/packetfilter/package/univention-snmpd/tcp/161/10.1.1.{20,200}='ACCEPT'> -A INPUT -d 10.1.1.20/32 -p tcp -m tcp --dport 161 -j ACCEPT
           ^^
> -A INPUT -d 10.1.1.200/32 -p tcp -m tcp --dport 161 -j ACCEPT
           ^^

This makes the current firewall useless for your purpose.

(In reply to Florian Best from comment #1)
> I think an solution would be to add another UCR variable:
> security/packetfilter/{tcp,udp}/.*/priority ?

Basically the mechanism to configure the firewall via some *flat* UCR rules is flawed because the ordering is important for correctness and performance, which the current mechanism does not support:
- in UCS 4.4 the ordering depends on Pythons internal hash sorting
- in UCS 5.0 they are sorted by key: boosting "tcp" over "udp", sorting ports as "str" instead of "int", preferring IP addresses over "all" over "ipv4" and "ipv4"

Currently "/priority" would also be wrongly interpreted as a language name and the value would be printed as a description.

Instead of making the ordering implicit via "/priority" it would be better to have the current implementation sort the rules on who specific they are; the implementation in UCS 5.0 actually does this by accident with git:542951d540 fro Bug #51035, when it added that "sorted(…)" which sorts: numbers like IP addresses before "all", "ipv4" and "ipv6", which only change which underlying tool "ip[6]tables" is used:

> # python3 -c 'print(sorted(["1.2.3.4", "1::2", "all", "ipv4", "ipv6"]))'
> ['1.2.3.4', '1::2', 'all', 'ipv4', 'ipv6']

Nevertheless the current implementation remains flawed because it does not allow to specify a source address [range] and the ordering cannot be controlled enough for more advanced scenarios.

Fun fact: with the current schema you easily get contradicting rules:
# ucr search --brief --non-empty ^security/packetfilter/ | sed -rne 's,security/packetfilter.*/(udp|tcp)/([0-9:]+)/(all|ipv[46]|[0-9.:]+): .*,\1\t\2\t\3,p' | sort | uniq -d
tcp     389     all
tcp     464     all
tcp     53      all
tcp     636     all
tcp     749     all
tcp     88      all
udp     123     all
udp     464     all
udp     53      all
udp     88      all
These are used as the "unique key" but are defined via multiple UCRV - for example udp/123:
# ucr search --brief --non-empty ^security/packetfilter/ | grep 123
security/packetfilter/package/univention-base-files/udp/123/all/en: ntp
security/packetfilter/package/univention-base-files/udp/123/all: ACCEPT
security/packetfilter/package/univention-samba4/udp/123/all/en: TIME
security/packetfilter/package/univention-samba4/udp/123/all: ACCEPT
Which one of these you got with UCS 4.4 was completely random, with UCS 5.0 the lexicographic latest wins.