Add nftables resources (#6499) (#44)

* Fix systemd path for Leap image



* Use vhef client version 17 as doocker cookbook do not support >= 18



* Add nftables resource



* Add nftables tests



* Add fixtures for nftables tests



* enable nftables only when attr is true - then disable iptables



* By default test iptables, not nftables



* Fix tests and lint errors



* Increase unit test coverage for nftables



* Do not use -nn nft option as behaviour changes based on nft version



* Base nft params identification on its version, not os version

    Signed-off-by: Jeremy JACQUE <jeremy.jacque@algolia.com>

* Make test more human friendly by reversing unless/if logic

    Signed-off-by: Jeremy JACQUE <jeremy.jacque@algolia.com>

* Update mocked cmds with nft params

    Signed-off-by: Jeremy JACQUE <jeremy.jacque@algolia.com>

* Fix quoting issue with rubocop



* Fix uninitiallized class vars



* Fix unit test by adding nft version mocking



* Clean nftables doc



---------

Signed-off-by: Jeremy JACQUE <jeremy.jacque@algolia.com>
Co-authored-by: jjacque <jeremy.jacque@algolia.com>
This commit is contained in:
Clinton Wolfe 2023-05-17 20:45:57 -04:00 committed by GitHub
parent f51da83bf9
commit 4fce6845e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 515 additions and 4 deletions

View file

@ -0,0 +1,140 @@
+++
title = "nftables resource"
draft = false
gh_repo = "inspec"
platform = "linux"
[menu]
[menu.inspec]
title = "nftables"
identifier = "inspec/resources/os/nftables.md nftables resource"
parent = "inspec/resources/os"
+++
Use the `nftables` Chef InSpec audit resource to test rules and sets that are defined using `nftables`, which maintains tables of IP packet filtering rules. There may be more than one table. Each table contains one (or more) chains. A chain is a list of rules that match packets. When a rule matches a packet, the rule defines what target to assign to the packet.
## Availability
### Installation
{{% inspec/inspec_installation %}}
### Version
This resource first became available in v5.21.30 of InSpec.
## Syntax
A `nftables` resource block declares tests for rules in IP tables:
```ruby
describe nftables(family:'name', table:'name', chain: 'name') do
its('PROPERTY') { should eq 'value' }
it { should have_rule('RULE') }
end
describe nftables(family:'name', table:'name', set: 'name') do
its('PROPERTY') { should eq 'value' }
it { should have_element('ELEMENT') }
end
```
where
- `nftables()` has to specify `family` and `table`. It also has to specify one of `chain` or `set` (exclusively).
- `family:'name'` is the name of the `family` the table belongs to, one of `ip`, `ip6`, `inet`, `arp`, `bridge`, `netdev`.
- `table:'name'` is the packet matching table against which the test is run.
- `chain: 'name'` is the name of a user-defined chain.
- `set: 'name'` is the name of a user-defined named set.
- `have_rule('RULE')` tests that the chain has a given rule in the nftables ruleset. This must match the entire line taken from `nftables -nn list chain FAMILY TABLE CHAIN`.
- `have_element('ELEMENT')` tests that element is a member of the nftables named set.
See the [NFT man page](https://www.netfilter.org/projects/nftables/manpage.html) and [nftables wiki](https://wiki.nftables.org/wiki-nftables/index.php/Main_Page) for more information about nftables.
## Properties
### Chain Properties
`hook`
: The hook type. Possible values: `ingress`, `prerouting`, `forward`, `input`, `output`, `postrouting`, and `egress`.
`prio`
: The numerical chain priority.
`policy`
: The policy type. Possible values: `accept`, `drop`.
`type`
: The chain type. Possible values: `filter`, `nat`, and `route`.
### Set Properties
`flags`
: The set flags. Possible values: `constant`, `dynamic`, `interval`, and `timeout`.
`size`
: The maximum number of elements in the set.
`type`
: The data type of set elements. Possible values: `ipv4_addr`, `ipv6_addr`, `ether_addr`, `inet_proto`, `inet_service`, and `mark`.
## Examples
The following examples show how to use this Chef InSpec audit resource.
### Test if the `CHAIN_NAME` chain from the `TABLE_NAME` table has the default `accept` policy
```ruby
describe nftables(family: 'inet', table: 'TABLE_NAME', chain: 'CHAIN_NAME') do
its('policy') { should eq 'accept' }
end
```
### Test the attributes of the `CHAIN_NAME` chain from the `TABLE_NAME` table
```ruby
describe nftables(family: 'inet', table: 'mangle', chain: 'INPUT') do
its('type') { should eq 'filter' }
its('hook') { should eq 'input' }
its('prio') { should eq (-150) } # mangle
its('policy') { should eq 'accept' }
end
```
### Test if there is a rule allowing Postgres (5432/TCP) traffic
```ruby
describe nftables(family: 'inet', table: 'TABLE_NAME', chain: 'CHAIN_NAME') do
it { should have_rule('tcp dport 5432 comment "postgres" accept') }
end
```
Note that the rule specification must exactly match what's in the output of `nftables -nn list chain inet TABLE_NAME CHAIN_NAME`, which will depend on how you've built your rules.
### Test if there is an element `1.1.1.1` in the `SET_NAME` named set
```ruby
describe nftables(family: 'inet', table: 'TABLE_NAME', set: 'SET_NAME') do
it { should have_element('1.1.1.1') }
end
```
## Matchers
For a full list of available matchers, please visit our [matchers page](/inspec/matchers/).
### have_rule
The `have_rule` matcher tests the named rule against the information in the `nftables` ruleset:
```ruby
it { should have_rule('RULE') }
```
### have_element
The `have_element` matcher tests the named set against the information in the `nftables` ruleset:
```ruby
it { should have_element('SET_ELEMENT') }
```

View file

@ -1,7 +1,7 @@
---
driver:
name: dokken
chef_version: :latest
chef_version: 17
privileged: true # because Docker and SystemD/Upstart
transport:
@ -64,7 +64,7 @@ platforms:
- name: opensuse-leap
driver:
image: dokken/opensuse-leap-15
pid_one_command: /bin/systemd
pid_one_command: /usr/lib/systemd/systemd
- name: ubuntu-16.04
driver:

View file

@ -73,6 +73,7 @@ require "inspec/resources/mssql_sys_conf"
require "inspec/resources/mysql"
require "inspec/resources/mysql_conf"
require "inspec/resources/mysql_session"
require "inspec/resources/nftables"
require "inspec/resources/nginx"
require "inspec/resources/nginx_conf"
require "inspec/resources/npm"

View file

@ -0,0 +1,251 @@
require "inspec/resources/command"
require "json" unless defined?(JSON)
# @see https://wiki.nftables.org/
# @see https://www.netfilter.org/projects/nftables/manpage.html
# rubocop:disable Style/ClassVars
module Inspec::Resources
class NfTables < Inspec.resource(1)
name "nftables"
supports platform: "linux"
desc "Use the nftables InSpec audit resource to test rules and sets that are defined in nftables, which maintains tables of IP packet filtering rules. There may be more than one table. Each table contains one (or more) chains. A chain is a list of rules that match packets. When the rule matches, the rule defines what target to assign to the packet."
example <<~EXAMPLE
describe nftables(family:'inet', table:'filter', chain: 'INPUT') do
its('type') { should eq 'filter' }
its('hook') { should eq 'input' }
its('prio') { should eq 0 } # filter
its('policy') { should eq 'drop' }
it { should have_rule('tcp dport { 22, 80, 443 } accept') }
end
describe nftables(family: 'inet', table: 'filter', set: 'OPEN_PORTS') do
its('type') { should eq 'ipv4_addr . inet_proto . inet_service' }
its('flags') { should include 'interval' }
it { should have_element('1.1.1.1 . tcp . 25-27') }
end
EXAMPLE
@@bin = nil
@@nft_params = {}
@@nft_params["json"] = ""
@@nft_params["stateless"] = ""
@@nft_params["num"] = ""
def initialize(params = {})
@family = params[:family] || nil
@table = params[:table] || nil
@chain = params[:chain] || nil
@set = params[:set] || nil
@ignore_comments = params[:ignore_comments] || false
unless @@bin
@@bin = find_nftables_or_error
end
# Some old versions of `nft` do not support JSON output or stateless modifier
res = inspec.command("#{@@bin} --version").stdout
version = Gem::Version.new(/^nftables v(\S+) .*/.match(res)[1])
case
when version < Gem::Version.new("0.8.0")
@@nft_params["num"] = "-nn"
when version < Gem::Version.new("0.9.0")
@@nft_params["stateless"] = "-s"
@@nft_params["num"] = "-nn"
when version < Gem::Version.new("0.9.3")
@@nft_params["json"] = "-j"
@@nft_params["stateless"] = "-s"
@@nft_params["num"] = "-nn"
when version >= Gem::Version.new("0.9.3")
@@nft_params["json"] = "-j"
@@nft_params["stateless"] = "-s"
@@nft_params["num"] = "-y"
## --terse
end
# family and table attributes are mandatory
fail_resource "nftables family and table are mandatory." if @family.nil? || @family.empty? || @table.nil? || @table.empty?
# chain name or set name has to be specified and are mutually exclusive
fail_resource "You must specify either a chain or a set name." if (@chain.nil? || @chain.empty?) && (@set.nil? || @set.empty?)
fail_resource "You must specify either a chain or a set name, not both." if !(@chain.nil? || @chain.empty?) && !(@set.nil? || @set.empty?)
# we're done if we are on linux
return if inspec.os.linux?
# ensures, all calls are aborted for non-supported os
@nftables_cache = {}
skip_resource "The `nftables` resource is not supported on your OS yet."
end
# Let's have a generic method to retrieve attributes for chains and sets
def _get_attr(name)
# Some attributes are valid for chains only, for sets only or for both
valid = {
"chains" => %w{hook policy prio type},
"sets" => %w{flags size type},
}
target_obj = @set.nil? ? "chains" : "sets"
if valid[target_obj].include?(name)
attrs = @set.nil? ? retrieve_chain_attrs : retrieve_set_attrs
else
raise Inspec::Exceptions::ResourceSkipped, "`#{name}` attribute is not valid for #{target_obj}"
end
# flags attribute is an array, if not retrieved ensure we return an empty array
# otherwise return an empty string
default = name == "flags" ? [] : ""
val = attrs.key?(name) ? attrs[name] : default
# When set type is has multiple data types it's retrieved as an array, make humans life easier
# by returning a string representation
if name == "type" && target_obj == "sets" && val.is_a?(Array)
return val.join(" . ")
end
val
end
# Create a method for each attribute
%i{flags hook policy prio size type}.each do |attr_method|
define_method attr_method do
_get_attr(attr_method.to_s)
end
end
def has_rule?(rule = nil, _family = nil, _table = nil, _chain = nil)
# checks if the rule is part of the chain
# for now, we expect an exact match
retrieve_chain_rules.any? { |line| line.casecmp(rule) == 0 }
end
def has_element?(element = nil, _family = nil, _table = nil, _chain = nil)
# checks if the element is part of the set
# for now, we expect an exact match
retrieve_set_elements.any? { |line| line.casecmp(element) == 0 }
end
def retrieve_set_elements
idx = "set_#{@family}_#{@table}_#{@set}"
return @nftables_cache[idx] if defined?(@nftables_cache) && @nftables_cache.key?(idx)
@nftables_cache = {} unless defined?(@nftables_cache)
elem_cmd = "list set #{@family} #{@table} #{@set}"
nftables_cmd = format("%s %s %s", @@bin, @@nft_params["stateless"], elem_cmd).strip
cmd = inspec.command(nftables_cmd)
return [] if cmd.exit_status.to_i != 0
@nftables_cache[idx] = cmd.stdout.gsub("\t", "").split("\n").reject { |line| line =~ /^(table|set|type|size|flags|typeof|auto-merge)/ || line =~ /^}$/ }.map { |line| line.sub("elements = {", "").sub("}", "").split(",") }.flatten.map(&:strip)
end
def retrieve_chain_rules
idx = "rule_#{@family}_#{@table}_#{@chain}"
return @nftables_cache[idx] if defined?(@nftables_cache) && @nftables_cache.key?(idx)
@nftables_cache = {} unless defined?(@nftables_cache)
# construct nftables command to read all rules of the given chain
chain_cmd = "list chain #{@family} #{@table} #{@chain}"
nftables_cmd = format("%s %s %s %s", @@bin, @@nft_params["stateless"], @@nft_params["num"], chain_cmd).strip
cmd = inspec.command(nftables_cmd)
return [] if cmd.exit_status.to_i != 0
rules = cmd.stdout.gsub("\t", "").split("\n").reject { |line| line =~ /^(table|chain)/ || line =~ /^}$/ }
if @ignore_comments
# split rules, returns array or rules without any comment
@nftables_cache[idx] = remove_comments_from_rules(rules)
else
# split rules, returns array or rules
@nftables_cache[idx] = rules.map(&:strip)
end
end
def retrieve_chain_attrs
idx = "chain_attrs_#{@family}_#{@table}_#{@chain}"
return @nftables_cache[idx] if defined?(@nftables_cache) && @nftables_cache.key?(idx)
@nftables_cache = {} unless defined?(@nftables_cache)
chain_cmd = "list chain #{@family} #{@table} #{@chain}"
nftables_cmd = format("%s %s %s %s", @@bin, @@nft_params["stateless"], @@nft_params["json"], chain_cmd).strip
cmd = inspec.command(nftables_cmd)
return {} if cmd.exit_status.to_i != 0
if @@nft_params["json"].empty?
res = cmd.stdout.gsub("\t", "").split("\n").select { |line| line =~ /^type/ }[0]
parsed = /type (\S+) hook (\S+) priority (\S+); policy (\S+);/.match(res)
@nftables_cache[idx] = { "type" => parsed[1], "hook" => parsed[2], "prio" => parsed[3].to_i, "policy" => parsed[4] }
else
@nftables_cache[idx] = JSON.parse(cmd.stdout)["nftables"].select { |line| line.key?("chain") }[0]["chain"]
end
end
def retrieve_set_attrs
idx = "set_attrs_#{@family}_#{@table}_#{@chain}"
return @nftables_cache[idx] if defined?(@nftables_cache) && @nftables_cache.key?(idx)
@nftables_cache = {} unless defined?(@nftables_cache)
chain_cmd = "list set #{@family} #{@table} #{@set}"
nftables_cmd = format("%s %s %s %s", @@bin, @@nft_params["stateless"], @@nft_params["json"], chain_cmd).strip
cmd = inspec.command(nftables_cmd)
return {} if cmd.exit_status.to_i != 0
if @@nft_params["json"].empty?
type = ""
size = 0
flags = []
res = cmd.stdout.gsub("\t", "").split("\n").select { |line| line =~ /^(type|size|flags)/ }
res.each do |line|
parsed = /^type (.*)/.match(line)
if parsed
type = parsed[1]
end
parsed = /^flags (.*)/.match(line)
if parsed
flags = parsed[1].split(",")
end
parsed = /^size (.*)/.match(line)
if parsed
size = parsed[1].to_i
end
end
@nftables_cache[idx] = { "type" => type, "size" => size, "flags" => flags }
else
@nftables_cache[idx] = JSON.parse(cmd.stdout)["nftables"].select { |line| line.key?("set") }[0]["set"]
end
end
def resource_id
to_s || "nftables"
end
def to_s
format("nftables (%s %s %s %s)", @family && "family: #{@family}", @table && "table: #{@table}", @chain && "chain: #{@chain}", @set && "set: #{@set}").strip
end
private
def remove_comments_from_rules(rules)
rules.each do |rule|
next if rule.nil?
rule.gsub!(/ comment "([^"]*)"/, "")
rule.strip
end
rules
end
def find_nftables_or_error
%w{/usr/sbin/nft /sbin/nft nft}.each do |cmd|
return cmd if inspec.command(cmd).exist?
end
raise Inspec::Exceptions::ResourceFailed, "Could not find `nft`"
end
end
end

View file

@ -148,7 +148,7 @@ RSpec::Matchers.define :be_resolvable do
end
end
# matcher for iptables and ip6tables
# matcher for iptables, ip6tables and nftables
RSpec::Matchers.define :have_rule do |rule|
match do |tables|
tables.has_rule?(rule)
@ -163,6 +163,13 @@ RSpec::Matchers.define :have_rule do |rule|
end
end
# matcher for nftables sets
RSpec::Matchers.define :have_element do |elem|
match do |sets|
sets.has_element?(elem)
end
end
# `be_in` matcher
# You can use it in the following cases:
# - check if an item or array is included in a given array

7
test/fixtures/cmd/nftables-chain vendored Normal file
View file

@ -0,0 +1,7 @@
table inet filter {
chain INPUT {
type filter hook input priority 0; policy accept;
iifname "eth0" tcp dport 80 accept comment "http on 80"
jump derby-cognos-web
}
}

1
test/fixtures/cmd/nftables-chain-json vendored Normal file
View file

@ -0,0 +1 @@
{"nftables": [{"metainfo": {"version": "1.0.2", "release_name": "Lester Gooch", "json_schema_version": 1}}, {"chain": {"family": "inet", "table": "filter", "name": "INPUT", "handle": 1, "type": "filter", "hook": "input", "prio": 0, "policy": "accept"}}, {"rule": {"family": "inet", "table": "filter", "chain": "INPUT", "handle": 4, "comment": "http on 80", "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "eth0"}}, {"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": 80}}, {"accept": null}]}}, {"rule": {"family": "inet", "table": "filter", "chain": "INPUT", "handle": 5, "expr": [{"jump": {"target": "derby-cognos-web"}}]}}]}

8
test/fixtures/cmd/nftables-set vendored Normal file
View file

@ -0,0 +1,8 @@
table inet filter {
set OPEN_PORTS {
type ipv4_addr
size 65536
flags interval
elements = { 1.1.1.1 }
}
}

1
test/fixtures/cmd/nftables-set-json vendored Normal file
View file

@ -0,0 +1 @@
{"nftables": [{"metainfo": {"version": "1.0.2", "release_name": "Lester Gooch", "json_schema_version": 1}}, {"set": {"family": "inet", "name": "OPEN_PORTS", "table": "filter", "type": "ipv4_addr", "handle": 3, "size": 65536, "flags": ["interval"], "elem": ["1.1.1.1"]}}]}

1
test/fixtures/cmd/nftables-version vendored Normal file
View file

@ -0,0 +1 @@
nftables v1.0.2 (Lester Gooch)

View file

@ -385,6 +385,24 @@ class MockLoader
# ip6tables
"/usr/sbin/ip6tables -S" => cmd.call("ip6tables-s"),
%{sh -c 'type "/usr/sbin/ip6tables"'} => empty.call,
# nftables (version)
"/usr/sbin/nft --version" => cmd.call("nftables-version"),
# nftables (chain with json output)
"/usr/sbin/nft -s -j list chain inet filter INPUT" => cmd.call("nftables-chain-json"),
"/usr/sbin/nft -j list chain inet filter INPUT" => cmd.call("nftables-chain-json"),
# nftables (chain)
"/usr/sbin/nft -s list chain inet filter INPUT" => cmd.call("nftables-chain"),
"/usr/sbin/nft -s -y list chain inet filter INPUT" => cmd.call("nftables-chain"),
"/usr/sbin/nft -s -nn list chain inet filter INPUT" => cmd.call("nftables-chain"),
"/usr/sbin/nft -y list chain inet filter INPUT" => cmd.call("nftables-chain"),
"/usr/sbin/nft list chain inet filter INPUT" => cmd.call("nftables-chain"),
# nftables (set with json output)
"/usr/sbin/nft -s -j list set inet filter OPEN_PORTS" => cmd.call("nftables-set-json"),
"/usr/sbin/nft -j list set inet filter OPEN_PORTS" => cmd.call("nftables-set-json"),
# nftables (set)
"/usr/sbin/nft -s list set inet filter OPEN_PORTS" => cmd.call("nftables-set"),
"/usr/sbin/nft list set inet filter OPEN_PORTS" => cmd.call("nftables-set"),
%{sh -c 'type "/usr/sbin/nft"'} => empty.call,
# ipnat
"/usr/sbin/ipnat -l" => cmd.call("ipnat-l"),
%{type "/usr/sbin/ipnat"} => empty.call,

View file

@ -1,2 +1,3 @@
default["osprepare"]["docker"] = false
default["osprepare"]["application"] = true
default["osprepare"]["nftables"] = false

View file

@ -26,7 +26,11 @@ include_recipe("os_prepare::service")
include_recipe("os_prepare::package")
include_recipe("os_prepare::registry_key")
include_recipe("os_prepare::iis")
include_recipe("os_prepare::iptables")
if node["osprepare"]["nftables"]
include_recipe("os_prepare::nftables")
else
include_recipe("os_prepare::iptables")
end
include_recipe("os_prepare::x509")
include_recipe("os_prepare::dh_params")

View file

@ -0,0 +1,12 @@
if platform_family?("rhel", "debian", "fedora", "suse")
package "nftables"
execute "nft flush ruleset"
execute "nft add table inet filter"
execute 'nft add chain inet filter INPUT \{ type filter hook input priority 0\; policy accept\; \}'
execute "nft add chain inet filter derby-cognos-web"
execute 'nft add set inet filter OPEN_PORTS \{ type ipv4_addr\; size 65536\; flags interval\; \}'
execute 'nft add rule inet filter INPUT iifname eth0 tcp dport 80 accept comment \"http on 80\"'
execute "nft add rule inet filter INPUT jump derby-cognos-web"
execute 'nft add rule inet filter derby-cognos-web tcp dport 80 accept comment "derby-cognos-web"'
execute 'nft add element inet filter OPEN_PORTS \{ 1.1.1.1/32 \}'
end

View file

@ -2,6 +2,10 @@ unless ENV['IPV6']
$stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running without IPv6\033[0m"
return
end
if ENV['NFTABLES']
$stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running with nftables\033[0m"
return
end
case os[:family]
when 'ubuntu', 'fedora', 'debian', 'suse'

View file

@ -1,3 +1,8 @@
if ENV['NFTABLES']
$stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running with nftables\033[0m"
return
end
case os[:family]
when 'ubuntu', 'fedora', 'debian', 'suse'
describe iptables do

View file

@ -0,0 +1,23 @@
unless ENV['NFTABLES']
$stderr.puts "\033[1;33mTODO: Not running #{__FILE__.split("/").last} because we are running with iptables\033[0m"
return
end
case os[:family]
when 'ubuntu', 'fedora', 'debian', 'suse', 'redhat', 'centos'
describe nftables(family: 'inet', table: 'filter', chain: 'INPUT') do
its('type') { should eq 'filter' }
its('hook') { should eq 'input' }
its('prio') { should eq 0 }
its('policy') { should eq 'accept' }
it { should have_rule('iifname "eth0" tcp dport 80 accept comment "http on 80"') }
it { should_not have_rule('iifname "eth1" tcp dport 80 accept') }
end
describe nftables(family: 'inet', table: 'filter', set: 'OPEN_PORTS') do
its('type') { should eq 'ipv4_addr' }
its('flags') { should include 'interval' }
it { should have_element('1.1.1.1') }
it { should_not have_element('2.2.2.2') }
end
end

View file

@ -0,0 +1,27 @@
require "helper"
require "inspec/resource"
require "inspec/resources/nftables"
describe "Inspec::Resources::NfTables" do
# ubuntu
it "verify nftables chain on ubuntu" do
resource = MockLoader.new(:ubuntu).load_resource("nftables", { family: "inet", table: "filter", chain: "INPUT" })
_(resource.type).must_equal "filter"
_(resource.hook).must_equal "input"
_(resource.prio).must_equal 0
_(resource.policy).must_equal "accept"
_(resource.has_rule?('iifname "eth0" tcp dport 80 accept comment "http on 80"')).must_equal true
_(resource.has_rule?('iifname "eth1" tcp dport 80 accept')).must_equal false
_(resource.resource_id).must_equal "nftables (family: inet table: filter chain: INPUT )"
end
it "verify nftables set on ubuntu" do
resource = MockLoader.new(:ubuntu).load_resource("nftables", { family: "inet", table: "filter", set: "OPEN_PORTS" })
_(resource.type).must_equal "ipv4_addr"
_(resource.flags).must_include "interval"
_(resource.size).must_equal 65536
_(resource.has_element?("1.1.1.1")).must_equal true
_(resource.has_element?("2.2.2.2")).must_equal false
_(resource.resource_id).must_equal "nftables (family: inet table: filter set: OPEN_PORTS)"
end
end