netfilter.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* Analyzing firewall rules programmatically
@ 2024-01-30 20:13 Timo Lindfors
  2024-02-01 20:34 ` Kerin Millar
  0 siblings, 1 reply; 6+ messages in thread
From: Timo Lindfors @ 2024-01-30 20:13 UTC (permalink / raw)
  To: Netfilter list

Hi,

I spent some time trying to figure out how to programmatically answer 
questions like "Does the ruleset R allow reaching service listening on port P from 
interface I?" or "Does the ruleset R1 allow something that ruleset R2 does 
not allow?".

I tried to find prior work on this area and was able to find "Verified 
iptables Firewall Analysis and Verification" [1] that uses 
the Isabelle proof assistant for iptables rules and "Automated Analysis 
and Debugging of Network Connectivity Policies" [2, 3] that uses Z3 for 
some simple Azure firewall ACL rules.

I could not find anything relevant for netfilter so I began experimenting 
a bit by writing a python function that tries to simulate nftables 
behavior given a ruleset and a packet. I used output of "nft --json list 
ruleset" to and soon noticed that it doesn't quite contain all the 
necessary information for this task.

For example, the default ufw configuration uses iptables to setup

Chain ufw-not-local (1 references)
  pkts bytes target     prot opt in     out     source               destination
   779 46756 RETURN     0    --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

which shows up as

$ sudo nft -a list ruleset | grep "handle 247"
 		fib daddr type local counter packets 772 bytes 46336

in in the "normal" output. However, in the JSON output critical 
information (mainly "LOCAL") is entirely missing:

$ sudo nft -a --json list ruleset | jq . | grep -B5 -A19 '"handle": 247'
     {
       "rule": {
         "family": "ip",
         "table": "filter",
         "chain": "ufw-not-local",
         "handle": 247,
         "expr": [
           {
             "xt": {
               "type": "match",
               "name": "addrtype"
             }
           },
           {
             "counter": {
               "packets": 778,
               "bytes": 46696
             }
           },
           {
             "return": null
           }
         ]
       }
     },


I understand that ufw is using a compatibility interface. Nevertheless, 
I'm in need of a way to analyze firewall configurations of large number of 
real-world systems programmatically. Can you suggest how I should approach 
this? I can think of at least the following options:

1) Fix the JSON output.

2) Ignore the JSON output and try to parse the output of "nft list 
ruleset".

3) Use JSON output of most of the stuff but fill the gaps by also 
parsing "nft list ruleset".

4) Try to parse the raw netlink traffic seen in "nft --debug=netlink list 
ruleset".

4) Create separate tools for parsing iptables and netfilter rules and 
hope that no system mixes these two.


[1] https://link.springer.com/article/10.1007/s10817-017-9445-1
[2] https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/secguru.pdf
[3] https://github.com/Z3Prover/FirewallChecker



^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Analyzing firewall rules programmatically
  2024-01-30 20:13 Analyzing firewall rules programmatically Timo Lindfors
@ 2024-02-01 20:34 ` Kerin Millar
  2024-02-02 19:05   ` Timo Lindfors
  0 siblings, 1 reply; 6+ messages in thread
From: Kerin Millar @ 2024-02-01 20:34 UTC (permalink / raw)
  To: Timo Lindfors; +Cc: Netfilter list

On Tue, 30 Jan 2024 22:13:26 +0200 (EET)
Timo Lindfors <timo.lindfors@iki.fi> wrote:

> Hi,
> 
> I spent some time trying to figure out how to programmatically answer 
> questions like "Does the ruleset R allow reaching service listening on port P from 
> interface I?" or "Does the ruleset R1 allow something that ruleset R2 does 
> not allow?".
> 
> I tried to find prior work on this area and was able to find "Verified 
> iptables Firewall Analysis and Verification" [1] that uses 
> the Isabelle proof assistant for iptables rules and "Automated Analysis 
> and Debugging of Network Connectivity Policies" [2, 3] that uses Z3 for 
> some simple Azure firewall ACL rules.
> 
> I could not find anything relevant for netfilter so I began experimenting 
> a bit by writing a python function that tries to simulate nftables 
> behavior given a ruleset and a packet. I used output of "nft --json list 
> ruleset" to and soon noticed that it doesn't quite contain all the 
> necessary information for this task.
> 
> For example, the default ufw configuration uses iptables to setup
> 
> Chain ufw-not-local (1 references)
>   pkts bytes target     prot opt in     out     source               destination
>    779 46756 RETURN     0    --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL
> 
> which shows up as
> 
> $ sudo nft -a list ruleset | grep "handle 247"
>  		fib daddr type local counter packets 772 bytes 46336

Given that the rule is supposed to have been created with iptables-nft, I am surprised that it is presented in that way at all (the line you are showing doesn't match "handle 247" either).

# uname -r; nft -V | head -n1
6.7.2-arch1-2
nftables v1.0.9 (Old Doc Yak #3)
# nft flush ruleset
# iptables-nft -A INPUT -m addrtype --dst-type LOCAL
# nft list ruleset 2>/dev/null | sed -n 4p
		xt match "addrtype" counter packets 8 bytes 778

> 
> in in the "normal" output. However, in the JSON output critical 
> information (mainly "LOCAL") is entirely missing:
> 
> $ sudo nft -a --json list ruleset | jq . | grep -B5 -A19 '"handle": 247'
>      {
>        "rule": {
>          "family": "ip",
>          "table": "filter",
>          "chain": "ufw-not-local",
>          "handle": 247,
>          "expr": [
>            {
>              "xt": {
>                "type": "match",
>                "name": "addrtype"
>              }
>            },
>            {
>              "counter": {
>                "packets": 778,
>                "bytes": 46696
>              }
>            },
>            {
>              "return": null
>            }
>          ]
>        }
>      },
> 
> 
> I understand that ufw is using a compatibility interface. Nevertheless, 

Indeed, the output you are getting is symptomatic of the presence of xtables glue. The output of nft list ruleset won't be round-trip safe.

> I'm in need of a way to analyze firewall configurations of large number of 
> real-world systems programmatically. Can you suggest how I should approach 
> this? I can think of at least the following options:
> 
> 1) Fix the JSON output.

The xtables glue seems so disjoint that I wonder whether it would ever be fixed.

> 
> 2) Ignore the JSON output and try to parse the output of "nft list 
> ruleset".

This would not be reliable either.

> 
> 3) Use JSON output of most of the stuff but fill the gaps by also 
> parsing "nft list ruleset".

Nor this.

> 
> 4) Try to parse the raw netlink traffic seen in "nft --debug=netlink list 
> ruleset".

This doesn't seem very useful either.

# nft --debug=netlink list ruleset 2>/dev/null | head -n3
ip filter INPUT 2
  [ match name addrtype rev 1 ]
  [ counter pkts 287 bytes 59823 ]

> 
> 4) Create separate tools for parsing iptables and netfilter rules and 
> hope that no system mixes these two.

As things stand, this seems more realistic to me. Perhaps one of the developers would have a better idea, though.

-- 
Kerin Millar

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Analyzing firewall rules programmatically
  2024-02-01 20:34 ` Kerin Millar
@ 2024-02-02 19:05   ` Timo Lindfors
  2024-02-02 20:55     ` Kerin Millar
  2024-02-03 11:45     ` Florian Westphal
  0 siblings, 2 replies; 6+ messages in thread
From: Timo Lindfors @ 2024-02-02 19:05 UTC (permalink / raw)
  To: Netfilter list



On Thu, 1 Feb 2024, Kerin Millar wrote:
> Given that the rule is supposed to have been created with iptables-nft, I am surprised that it is presented in that way at all (the line you are showing doesn't match "handle 247" either).

ah, I might have made an error while trying to format the lines for 80 
characters. Here's a fresh new situation:

$ sudo nft -a list ruleset | grep "handle 125"
# Warning: table ip filter is managed by iptables-nft, do not touch!
 		fib daddr type broadcast counter packets 33859 bytes 6322835 jump ufw-skip-to-policy-input # handle 125
# Warning: table ip6 filter is managed by iptables-nft, do not touch!
 	chain ufw6-user-logging-output { # handle 125
# Warning: table ip nat is managed by iptables-nft, do not touch!
# Warning: table ip mangle is managed by iptables-nft, do not touch!

$ sudo nft -a --json list ruleset | jq . | grep -B5 -A19 '"handle": 125'
     {
       "rule": {
         "family": "ip",
         "table": "filter",
         "chain": "ufw-after-input",
         "handle": 125,
         "expr": [
           {
             "xt": {
               "type": "match",
               "name": "addrtype"
             }
           },
           {
             "counter": {
               "packets": 33859,
               "bytes": 6322835
             }
           },
           {
             "jump": {
               "target": "ufw-skip-to-policy-input"
             }
           }
         ]



$ sudo iptables -nvL | grep 33859
33859 6323K ufw-skip-to-policy-input  0    --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type BROADCAST



> # uname -r; nft -V | head -n1
> 6.7.2-arch1-2
> nftables v1.0.9 (Old Doc Yak #3)
> # nft flush ruleset
> # iptables-nft -A INPUT -m addrtype --dst-type LOCAL
> # nft list ruleset 2>/dev/null | sed -n 4p
> 		xt match "addrtype" counter packets 8 bytes 778

I get different output on Debian 12:

# uname -r; nft -V | head -n1
6.1.0-17-amd64
nftables v1.0.6 (Lester Gooch #5)
# nft flush ruleset
# iptables-nft -A INPUT -m addrtype --dst-type LOCAL
# nft list ruleset 2>/dev/null | sed -n 4p
 		fib daddr type local counter packets 24 bytes 16144

Is this perhaps a regression?

>> 4) Create separate tools for parsing iptables and netfilter rules and
>> hope that no system mixes these two.
>
> As things stand, this seems more realistic to me. Perhaps one of the developers would have a better idea, though.

Ok, thanks for the insights. If I built the tool for netfilter first, 
which of the three formats should I try to analyze? netlink, text or json?

-Timo

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Analyzing firewall rules programmatically
  2024-02-02 19:05   ` Timo Lindfors
@ 2024-02-02 20:55     ` Kerin Millar
  2024-02-03 11:45     ` Florian Westphal
  1 sibling, 0 replies; 6+ messages in thread
From: Kerin Millar @ 2024-02-02 20:55 UTC (permalink / raw)
  To: Timo Lindfors, Netfilter list

On Fri, 2 Feb 2024, at 7:05 PM, Timo Lindfors wrote:
> On Thu, 1 Feb 2024, Kerin Millar wrote:
>> Given that the rule is supposed to have been created with iptables-nft, I am surprised that it is presented in that way at all (the line you are showing doesn't match "handle 247" either).
>
> ah, I might have made an error while trying to format the lines for 80 
> characters. Here's a fresh new situation:
>
> $ sudo nft -a list ruleset | grep "handle 125"
> # Warning: table ip filter is managed by iptables-nft, do not touch!
>  		fib daddr type broadcast counter packets 33859 bytes 6322835 jump 

This gives the initial impression that iptables-nft was enlightened enough to generate a native nft rule for its input. It can do that in some cases and not others.

> ufw-skip-to-policy-input # handle 125
> # Warning: table ip6 filter is managed by iptables-nft, do not touch!
>  	chain ufw6-user-logging-output { # handle 125
> # Warning: table ip nat is managed by iptables-nft, do not touch!
> # Warning: table ip mangle is managed by iptables-nft, do not touch!
>
> $ sudo nft -a --json list ruleset | jq . | grep -B5 -A19 '"handle": 125'
>      {
>        "rule": {
>          "family": "ip",
>          "table": "filter",
>          "chain": "ufw-after-input",
>          "handle": 125,
>          "expr": [
>            {
>              "xt": {
>                "type": "match",
>                "name": "addrtype"
>              }
>            },
>            {
>              "counter": {
>                "packets": 33859,
>                "bytes": 6322835
>              }
>            },
>            {
>              "jump": {
>                "target": "ufw-skip-to-policy-input"
>              }
>            }
>          ]

Yet, the above output indicates the use of an xtables match. Here's how it should look for a native rule.

# nft 'table t { chain c { fib daddr type broadcast counter; }; }'
# nft -j list ruleset
{
  "nftables": [
    {
      "metainfo": {
        "version": "1.0.9",
        "release_name": "Old Doc Yak #3",
        "json_schema_version": 1
      }
    },
    {
      "table": {
        "family": "ip",
        "name": "t",
        "handle": 2
      }
    },
    {
      "chain": {
        "family": "ip",
        "table": "t",
        "name": "c",
        "handle": 1
      }
    },
    {
      "rule": {
        "family": "ip",
        "table": "t",
        "chain": "c",
        "handle": 2,
        "expr": [
          {
            "match": {
              "op": "==",
              "left": {
                "fib": {
                  "result": "type",
                  "flags": [
                    "daddr"
                  ]
                }
              },
              "right": "broadcast"
            }
          },
          {
            "counter": {
              "packets": 0,
              "bytes": 0
            }
          }
        ]
      }
    }
  ]
}

>
>
>
> $ sudo iptables -nvL | grep 33859
> 33859 6323K ufw-skip-to-policy-input  0    --  *      *       0.0.0.0/0 
>            0.0.0.0/0            ADDRTYPE match dst-type BROADCAST
>
>
>
>> # uname -r; nft -V | head -n1
>> 6.7.2-arch1-2
>> nftables v1.0.9 (Old Doc Yak #3)
>> # nft flush ruleset
>> # iptables-nft -A INPUT -m addrtype --dst-type LOCAL
>> # nft list ruleset 2>/dev/null | sed -n 4p
>> 		xt match "addrtype" counter packets 8 bytes 778
>
> I get different output on Debian 12:
>
> # uname -r; nft -V | head -n1
> 6.1.0-17-amd64
> nftables v1.0.6 (Lester Gooch #5)
> # nft flush ruleset
> # iptables-nft -A INPUT -m addrtype --dst-type LOCAL
> # nft list ruleset 2>/dev/null | sed -n 4p
>  		fib daddr type local counter packets 24 bytes 16144
>
> Is this perhaps a regression?

It might be that iptables-nft is supposed to generate a native rule and that we are both experiencing a regression in that respect.

On the other hand, it might be the opposite. That is, even though iptables-nft could, in principle, generate a native rule for "-m addrtype --dst-type BROADCAST", it might be that it has never been able to and that the output of "nft list ruleset" is simply misleading in your case.

Presently, it is unclear to me.

>
>>> 4) Create separate tools for parsing iptables and netfilter rules and
>>> hope that no system mixes these two.
>>
>> As things stand, this seems more realistic to me. Perhaps one of the developers would have a better idea, though.
>
> Ok, thanks for the insights. If I built the tool for netfilter first, 
> which of the three formats should I try to analyze? netlink, text or json?

It's difficult for me to make a recommendation because I only fully understand the output of iptables-save, nft list ruleset, nft -j list ruleset etc. However, I would say that structured data is helpful, meaning that I would prefer "nft -j list ruleset" over "nft list ruleset".

-- 
Kerin Millar

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Analyzing firewall rules programmatically
  2024-02-02 19:05   ` Timo Lindfors
  2024-02-02 20:55     ` Kerin Millar
@ 2024-02-03 11:45     ` Florian Westphal
  2024-02-05 12:35       ` Timo Lindfors
  1 sibling, 1 reply; 6+ messages in thread
From: Florian Westphal @ 2024-02-03 11:45 UTC (permalink / raw)
  To: Timo Lindfors; +Cc: Netfilter list

Timo Lindfors <timo.lindfors@iki.fi> wrote:
> > # uname -r; nft -V | head -n1
> > 6.7.2-arch1-2
> > nftables v1.0.9 (Old Doc Yak #3)
> > # nft flush ruleset
> > # iptables-nft -A INPUT -m addrtype --dst-type LOCAL
> > # nft list ruleset 2>/dev/null | sed -n 4p
> > 		xt match "addrtype" counter packets 8 bytes 778
> 
> I get different output on Debian 12:
> 
> # uname -r; nft -V | head -n1
> 6.1.0-17-amd64
> nftables v1.0.6 (Lester Gooch #5)
> # nft flush ruleset
> # iptables-nft -A INPUT -m addrtype --dst-type LOCAL
> # nft list ruleset 2>/dev/null | sed -n 4p
> 		fib daddr type local counter packets 24 bytes 16144
> 
> Is this perhaps a regression?

More likely that the former nft is compiled without xtables support,
the latter nft binary asks iptables-translate for a textual
nft-equivalent repesentation of the addrtype match.

> Ok, thanks for the insights. If I built the tool for netfilter first, which
> of the three formats should I try to analyze? netlink, text or json?

Text is rather unstable, I would not rely on it.

json ought to be stable, netlink is stable (its the api after all).

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: Analyzing firewall rules programmatically
  2024-02-03 11:45     ` Florian Westphal
@ 2024-02-05 12:35       ` Timo Lindfors
  0 siblings, 0 replies; 6+ messages in thread
From: Timo Lindfors @ 2024-02-05 12:35 UTC (permalink / raw)
  To: Netfilter list



On Sat, 3 Feb 2024, Florian Westphal wrote:
> Text is rather unstable, I would not rely on it.
>
> json ought to be stable, netlink is stable (its the api after all).

Thanks. I implemented my first prototype on the JSON output, and only for 
netfilter for now. You can give it a ruleset, desired verdict and extra
constraints and it will find a packet that achieves the desired verdict if 
it exists.

Here are some examples:

nftables_analysis$ for i in test_data/*.nft; do echo "#### $i"; cat $i; ./find_satisfying_packet.py --json-ruleset $i.json -c tcp.dport==1234; done

#### test_data/complex1.nft
#!/usr/sbin/nft -f

table inet my_filter {
     chain input {
         type filter hook input priority 0; policy accept;
 	meta nfproto ipv6 tcp sport 53 meta length == 200 iif > 0 ip6 saddr fe80::fca5:f7ff:febc:97a4 tcp dport 1234 accept
 	tcp dport 1234 drop
     }
}
Satified
ip.daddr=-1
ip.saddr=-1
ip6.daddr=0
ip6.saddr=338288524927261089672224126588925286308
meta.iif=1
meta.l4proto=6
meta.length=200
meta.nfproto=10
tcp.dport=1234
tcp.sport=53
udp.dport=-1
udp.sport=-1
#### test_data/complex2.nft
#!/usr/sbin/nft -f

table inet my_filter {
     chain input {
         type filter hook input priority 0; policy accept;
 	meta nfproto ipv6 tcp dport 1234 accept
 	tcp dport 1234 drop
     }
}
Satified
ip.daddr=-1
ip.saddr=-1
ip6.daddr=0
ip6.saddr=0
meta.iif=0
meta.l4proto=6
meta.length=0
meta.nfproto=10
tcp.dport=1234
tcp.sport=0
udp.dport=-1
udp.sport=-1
#### test_data/source_iface.nft
#!/usr/sbin/nft -f

table inet my_filter {
     chain input {
         type filter hook input priority 0; policy accept;
         iif enp0s31f6 tcp dport 1234 accept
 	tcp dport 1234 drop
     }
}
Satified
ip.daddr=0
ip.saddr=0
ip6.daddr=-1
ip6.saddr=-1
meta.iif=0
meta.l4proto=6
meta.length=0
meta.nfproto=2
tcp.dport=1234
tcp.sport=0
udp.dport=-1
udp.sport=-1
#### test_data/source_ip2.nft
#!/usr/sbin/nft -f

table inet my_filter {
     chain input {
         type filter hook input priority 0; policy accept;
 	tcp dport 1234 jump helper
 	tcp dport 1234 accept
     }
     chain helper {
         ip saddr 10.61.7.4 accept
 	tcp dport 1234 drop
     }
}
Satified
ip.daddr=0
ip.saddr=171771652
ip6.daddr=-1
ip6.saddr=-1
meta.iif=0
meta.l4proto=6
meta.length=0
meta.nfproto=2
tcp.dport=1234
tcp.sport=0
udp.dport=-1
udp.sport=-1
#### test_data/source_ip.nft
#!/usr/sbin/nft -f

table inet my_filter {
     chain input {
         type filter hook input priority 0; policy accept;
         tcp dport 1234 ip saddr 10.61.7.4 accept
 	tcp dport 1234 drop
     }
}
Satified
ip.daddr=0
ip.saddr=171771652
ip6.daddr=-1
ip6.saddr=-1
meta.iif=0
meta.l4proto=6
meta.length=0
meta.nfproto=2
tcp.dport=1234
tcp.sport=0
udp.dport=-1
udp.sport=-1
#### test_data/source_port_ipv6.nft
#!/usr/sbin/nft -f

table ip my_filter {
     chain input {
         type filter hook input priority 0; policy accept;
 	tcp dport 1234 drop
     }
}

table ip6 my_filter {
     chain input {
         type filter hook input priority 0; policy accept;
         tcp dport 1234 tcp sport 53 accept
 	tcp dport 1234 drop
     }
}
Satified
ip.daddr=-1
ip.saddr=-1
ip6.daddr=0
ip6.saddr=0
meta.iif=0
meta.l4proto=6
meta.length=0
meta.nfproto=10
tcp.dport=1234
tcp.sport=53
udp.dport=-1
udp.sport=-1
#### test_data/source_port.nft
#!/usr/sbin/nft -f

table inet my_filter {
     chain input {
         type filter hook input priority 0; policy accept;
         tcp dport 1234 tcp sport 53 accept
 	tcp dport 1234 drop
     }
}
Satified
ip.daddr=0
ip.saddr=0
ip6.daddr=-1
ip6.saddr=-1
meta.iif=0
meta.l4proto=6
meta.length=0
meta.nfproto=2
tcp.dport=1234
tcp.sport=53
udp.dport=-1
udp.sport=-1
#### test_data/source_port_range.nft
#!/usr/sbin/nft -f

table inet my_filter {
     chain input {
         type filter hook input priority 0; policy accept;
 	tcp sport {50-55} tcp dport 1234 accept
 	tcp dport 1234 drop
     }
}
Satified
ip.daddr=0
ip.saddr=0
ip6.daddr=-1
ip6.saddr=-1
meta.iif=0
meta.l4proto=6
meta.length=0
meta.nfproto=2
tcp.dport=1234
tcp.sport=50
udp.dport=-1
udp.sport=-1


I put the code to https://github.com/lindi2/nftables_analysis

(MIT license, do whatever you want but I would appreciate bug fixes so 
that I can make it more reliable and maybe implement iptables support 
later as well)

-Timo


^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2024-02-05 12:35 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-01-30 20:13 Analyzing firewall rules programmatically Timo Lindfors
2024-02-01 20:34 ` Kerin Millar
2024-02-02 19:05   ` Timo Lindfors
2024-02-02 20:55     ` Kerin Millar
2024-02-03 11:45     ` Florian Westphal
2024-02-05 12:35       ` Timo Lindfors

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).