diff --git a/README.md b/README.md index 6bc9019d..e8a718b6 100644 --- a/README.md +++ b/README.md @@ -86,3 +86,10 @@ You can define the order by using the `order` param. Adds a named set to a given table. It allows composing the set using individual parameters but also takes raw input via the content and source parameters. + +## nftables::simplerule + +Allows expressing firewall rules without having to use nftables's language by +adding an abstraction layer a-la-Firewall. It's rather limited how far you can +go so if you need rather complex rules or you can speak nftables it's +recommended to use `nftables::rule` directly. diff --git a/REFERENCE.md b/REFERENCE.md index a4bcb8fb..7225fac4 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -66,6 +66,14 @@ * [`nftables::rules::masquerade`](#nftablesrulesmasquerade): masquerade all outgoing traffic * [`nftables::rules::snat4`](#nftablesrulessnat4): manage a ipv4 snat rule * [`nftables::set`](#nftablesset): manage a named set +* [`nftables::simplerule`](#nftablessimplerule) + +### Data types + +* [`Nftables::Addr`](#nftablesaddr): Represents an address expression to be used within a rule. +* [`Nftables::Addr::Set`](#nftablesaddrset): Represents a set expression to be used within a rule. +* [`Nftables::Port`](#nftablesport): Represents a port expression to be used within a rule. +* [`Nftables::Port::Range`](#nftablesportrange): Represents a port range expression to be used within a rule. ## Classes @@ -1215,3 +1223,149 @@ Data type: `Optional[Variant[String,Array[String,1]]]` Default value: ``undef`` +### `nftables::simplerule` + +The nftables::simplerule class. + +#### Parameters + +The following parameters are available in the `nftables::simplerule` defined type. + +##### `ensure` + +Data type: `Enum['present','absent']` + + + +Default value: `'present'` + +##### `rulename` + +Data type: `Pattern[/^[-a-zA-Z0-9_]+$/]` + + + +Default value: `$title` + +##### `order` + +Data type: `Pattern[/^\d\d$/]` + + + +Default value: `'50'` + +##### `chain` + +Data type: `String` + + + +Default value: `'default_in'` + +##### `table` + +Data type: `String` + + + +Default value: `'inet-filter'` + +##### `action` + +Data type: `Enum['accept', 'continue', 'drop', 'queue', 'return']` + + + +Default value: `'accept'` + +##### `comment` + +Data type: `Optional[String]` + + + +Default value: ``undef`` + +##### `dport` + +Data type: `Optional[Nftables::Port]` + + + +Default value: ``undef`` + +##### `proto` + +Data type: `Optional[Enum['tcp', 'tcp4', 'tcp6', 'udp', 'udp4', 'udp6']]` + + + +Default value: ``undef`` + +##### `daddr` + +Data type: `Optional[Nftables::Addr]` + + + +Default value: ``undef`` + +##### `set_type` + +Data type: `Enum['ip', 'ip6']` + + + +Default value: `'ip6'` + +##### `sport` + +Data type: `Optional[Nftables::Port]` + + + +Default value: ``undef`` + +##### `saddr` + +Data type: `Optional[Nftables::Addr]` + + + +Default value: ``undef`` + +##### `counter` + +Data type: `Boolean` + + + +Default value: ``false`` + +## Data types + +### `Nftables::Addr` + +Represents an address expression to be used within a rule. + +Alias of `Variant[Stdlib::IP::Address::V6, Stdlib::IP::Address::V4, Nftables::Addr::Set]` + +### `Nftables::Addr::Set` + +Represents a set expression to be used within a rule. + +Alias of `Pattern[/^@[-a-zA-Z0-9_]+$/]` + +### `Nftables::Port` + +Represents a port expression to be used within a rule. + +Alias of `Variant[Array[Stdlib::Port, 1], Stdlib::Port, Nftables::Port::Range]` + +### `Nftables::Port::Range` + +Represents a port range expression to be used within a rule. + +Alias of `Pattern[/^\d+-\d+$/]` + diff --git a/manifests/simplerule.pp b/manifests/simplerule.pp new file mode 100644 index 00000000..b3965c69 --- /dev/null +++ b/manifests/simplerule.pp @@ -0,0 +1,99 @@ +# @summary Provides a simplified interface to nftables::rule for basic use cases. +# It's recommended to use nftables::rule directly if you feel comfortable with +# nft's syntax. +# +# @example allow incoming traffic from port 541 on port 543 TCP to a given IP range and count packets +# nftables::simplerule{'my_service_in': +# action => 'accept', +# comment => 'allow traffic to port 543', +# counter => true, +# proto => 'tcp', +# dport => 543, +# daddr => '2001:1458::/32', +# sport => 541, +# } +# +# @param rulename +# The symbolic name for the rule to add. Defaults to the resource's title. +# +# @param order +# A number representing the order of the rule. +# +# @param chain +# The name of the chain to add this rule to. +# +# @param table +# The name of the table to add this rule to. +# +# @param action +# The verdict for the matched traffic. +# +# @param comment +# A typically human-readable comment for the rule. +# +# @param dport +# The destination port, ports or port range. +# +# @param proto +# The transport-layer protocol to match. +# +# @param daddr +# The destination address, CIDR or set to match. +# +# @param set_type +# When using sets as saddr or daddr, the type of the set. +# Use `ip` for sets of type `ipv4_addr`. +# +# @param sport +# The source port, ports or port range. +# +# @param saddr +# The source address, CIDR or set to match. +# +# @param counter +# Enable traffic counters for the matched traffic. + +define nftables::simplerule ( + Enum['present','absent'] $ensure = 'present', + Pattern[/^[-a-zA-Z0-9_]+$/] $rulename = $title, + Pattern[/^\d\d$/] $order = '50', + String $chain = 'default_in', + String $table = 'inet-filter', + Enum['accept', 'continue', 'drop', 'queue', 'return'] $action = 'accept', + Optional[String] $comment = undef, + Optional[Nftables::Port] $dport = undef, + Optional[Enum['tcp', 'tcp4', 'tcp6', 'udp', 'udp4', 'udp6']] $proto = undef, + Optional[Nftables::Addr] $daddr = undef, + Enum['ip', 'ip6'] $set_type = 'ip6', + Optional[Nftables::Port] $sport = undef, + Optional[Nftables::Addr] $saddr = undef, + Boolean $counter = false, +) { + if $dport and !$proto { + fail('Specifying a transport protocol via $proto is mandatory when passing a $dport') + } + + if $sport and !$proto { + fail('Specifying a transport protocol via $proto is mandatory when passing a $sport') + } + + if $ensure == 'present' { + nftables::rule { "${chain}-${rulename}": + content => epp('nftables/simplerule.epp', + { + 'action' => $action, + 'comment' => $comment, + 'counter' => $counter, + 'daddr' => $daddr, + 'dport' => $dport, + 'proto' => $proto, + 'saddr' => $saddr, + 'set_type' => $set_type, + 'sport' => $sport, + } + ), + order => $order, + table => $table, + } + } +} diff --git a/spec/defines/simplerule_spec.rb b/spec/defines/simplerule_spec.rb new file mode 100644 index 00000000..413e8a7c --- /dev/null +++ b/spec/defines/simplerule_spec.rb @@ -0,0 +1,277 @@ +require 'spec_helper' + +describe 'nftables::simplerule' do + let(:pre_condition) { 'include nftables' } + + let(:title) { 'my_default_rule_name' } + + on_supported_os.each do |os, os_facts| + context "on #{os}" do + let(:facts) { os_facts } + + describe 'minimum instantiation' do + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'accept', + order: '50', + ) + } + end + + describe 'dport without protocol' do + let(:params) do + { + dport: 333, + } + end + + it { is_expected.not_to compile } + end + + describe 'sport without protocol' do + let(:params) do + { + sport: 333, + } + end + + it { is_expected.not_to compile } + end + + describe 'all parameters provided' do + let(:title) { 'my_big_rule' } + let(:params) do + { + action: 'accept', + comment: 'this is my rule', + counter: true, + dport: 333, + sport: 444, + proto: 'udp', + chain: 'default_out', + daddr: '2001:1458::/32', + saddr: '2001:145c::/32', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_out-my_big_rule').with( + content: 'udp sport {444} udp dport {333} ip6 saddr 2001:145c::/32 ip6 daddr 2001:1458::/32 counter accept comment "this is my rule"', + order: '50', + ) + } + end + + describe 'port range' do + let(:params) do + { + dport: '333-334', + sport: '1-2', + proto: 'tcp', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'tcp sport {1-2} tcp dport {333-334} accept', + ) + } + end + + describe 'port array' do + let(:params) do + { + dport: [333, 335], + sport: [433, 435], + proto: 'tcp', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'tcp sport {433, 435} tcp dport {333, 335} accept', + ) + } + end + + describe 'only sport TCP traffic' do + let(:params) do + { + sport: 555, + proto: 'tcp', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'tcp sport {555} accept', + ) + } + end + + describe 'only IPv4 TCP traffic' do + let(:params) do + { + dport: 333, + proto: 'tcp4', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'ip version 4 tcp dport {333} accept', + ) + } + end + + describe 'only IPv6 UDP traffic' do + let(:params) do + { + dport: 33, + proto: 'udp6', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'ip version 6 udp dport {33} accept', + ) + } + end + + describe 'with an IPv4 CIDR as daddr' do + let(:params) do + { + daddr: '192.168.0.1/24', + dport: 33, + proto: 'tcp', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'tcp dport {33} ip daddr 192.168.0.1/24 accept', + ) + } + end + + describe 'with an IPv6 address as daddr' do + let(:params) do + { + daddr: '2001:1458::1', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'ip6 daddr 2001:1458::1 accept', + ) + } + end + + describe 'with an IPv6 address as saddr' do + let(:params) do + { + saddr: '2001:1458:0000:0000:0000:0000:0000:0003', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'ip6 saddr 2001:1458:0000:0000:0000:0000:0000:0003 accept', + ) + } + end + + describe 'with an IPv6 set as daddr, default set_type' do + let(:params) do + { + daddr: '@my6_set', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'ip6 daddr @my6_set accept', + ) + } + end + + describe 'with a IPv4 set as daddr' do + let(:params) do + { + daddr: '@my4_set', + set_type: 'ip', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'ip daddr @my4_set accept', + ) + } + end + + describe 'with a IPv6 set as saddr' do + let(:params) do + { + saddr: '@my6_set', + set_type: 'ip6', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'ip6 saddr @my6_set accept', + ) + } + end + + describe 'with counter enabled' do + let(:params) do + { + counter: true, + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'counter accept', + ) + } + end + + describe 'counter and continue sport' do + let(:params) do + { + proto: 'tcp', + sport: 80, + counter: true, + action: 'continue', + } + end + + it { is_expected.to compile } + it { + is_expected.to contain_nftables__rule('default_in-my_default_rule_name').with( + content: 'tcp sport {80} counter continue', + ) + } + end + end + end +end diff --git a/spec/type_aliases/nftables_addr_spec.rb b/spec/type_aliases/nftables_addr_spec.rb new file mode 100644 index 00000000..3e1d6104 --- /dev/null +++ b/spec/type_aliases/nftables_addr_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe 'Nftables::Addr' do + it { is_expected.to allow_value('127.0.0.1') } + it { is_expected.to allow_value('172.16.1.0/24') } + it { is_expected.to allow_value('2001:1458::/32') } + it { is_expected.to allow_value('2001:1458::3') } + it { is_expected.to allow_value('@set_name') } + it { is_expected.not_to allow_value('anything') } + it { is_expected.not_to allow_value(43) } + it { is_expected.not_to allow_value(['127.0.0.1']) } +end diff --git a/spec/type_aliases/nftables_port_spec.rb b/spec/type_aliases/nftables_port_spec.rb new file mode 100644 index 00000000..ccd649a5 --- /dev/null +++ b/spec/type_aliases/nftables_port_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe 'Nftables::Port' do + it { is_expected.to allow_value(53) } + it { is_expected.to allow_value([1, 1985, 65_535]) } + it { is_expected.to allow_value('53-55') } + it { is_expected.not_to allow_value('53') } + it { is_expected.not_to allow_value([]) } +end diff --git a/templates/simplerule.epp b/templates/simplerule.epp new file mode 100644 index 00000000..bacb8ffe --- /dev/null +++ b/templates/simplerule.epp @@ -0,0 +1,72 @@ +<%- | String $action, + Optional[String] $comment, + Boolean $counter, + Optional[Nftables::Addr] $daddr, + Optional[Nftables::Port] $dport, + Optional[String] $proto, + Optional[Nftables::Addr] $saddr, + String $set_type, + Optional[Nftables::Port] $sport, +| -%> +<%- if $proto { + $_proto = $proto ? { + /tcp(4|6)?/ => 'tcp', + /udp(4|6)?/ => 'udp', + } + $_ip_version_filter = $proto ? { + /(tcp4|udp4)/ => 'ip version 4', + /(tcp6|udp6)/ => 'ip version 6', + default => undef, + } +} else { + $_ip_version_filter = undef +} -%> +<%- if $daddr { + if $daddr =~ Stdlib::IP::Address::V6 { + $_dst_hosts = "ip6 daddr ${daddr}" + } elsif $daddr =~ Stdlib::IP::Address::V4 { + $_dst_hosts = "ip daddr ${daddr}" + } else { + $_dst_hosts = $set_type ? { + 'ip' => "ip daddr ${daddr}", + 'ip6' => "ip6 daddr ${daddr}", + } + } +} else { + $_dst_hosts = undef +} -%> +<%- if $saddr { + if $saddr =~ Stdlib::IP::Address::V6 { + $_src_hosts = "ip6 saddr ${saddr}" + } elsif $daddr =~ Stdlib::IP::Address::V4 { + $_src_hosts = "ip saddr ${saddr}" + } else { + $_src_hosts = $set_type ? { + 'ip' => "ip saddr ${saddr}", + 'ip6' => "ip6 saddr ${saddr}", + } + } +} else { + $_src_hosts = undef +} -%> +<%- if $proto and $dport { + $_dst_port = "${_proto} dport {${Array($dport, true).join(', ')}}" +} else { + $_dst_port = undef +} -%> +<%- if $comment { + $_comment = "comment \"${comment}\"" +} else { + $_comment = undef +} -%> +<%- if $proto and $sport { + $_src_port = "${_proto} sport {${Array($sport, true).join(', ')}}" +} else { + $_src_port = undef +} -%> +<%- if $counter { + $_counter = "counter" +} else { + $_counter = undef +} -%> +<%= regsubst(strip([$_ip_version_filter, $_src_port, $_dst_port, $_src_hosts, $_dst_hosts, $_counter, $action, $_comment].join(' ')), '\s+', ' ', 'G') -%> diff --git a/types/addr.pp b/types/addr.pp new file mode 100644 index 00000000..f7f7cdbc --- /dev/null +++ b/types/addr.pp @@ -0,0 +1,7 @@ +# @summary +# Represents an address expression to be used within a rule. +type Nftables::Addr = Variant[ + Stdlib::IP::Address::V6, + Stdlib::IP::Address::V4, + Nftables::Addr::Set +] diff --git a/types/addr/set.pp b/types/addr/set.pp new file mode 100644 index 00000000..f05b2391 --- /dev/null +++ b/types/addr/set.pp @@ -0,0 +1,3 @@ +# @summary +# Represents a set expression to be used within a rule. +type Nftables::Addr::Set = Pattern[/^@[-a-zA-Z0-9_]+$/] diff --git a/types/port.pp b/types/port.pp new file mode 100644 index 00000000..15847a70 --- /dev/null +++ b/types/port.pp @@ -0,0 +1,7 @@ +# @summary +# Represents a port expression to be used within a rule. +type Nftables::Port = Variant[ + Array[Stdlib::Port, 1], + Stdlib::Port, + Nftables::Port::Range, +] diff --git a/types/port/range.pp b/types/port/range.pp new file mode 100644 index 00000000..6c1de559 --- /dev/null +++ b/types/port/range.pp @@ -0,0 +1,3 @@ +# @summary +# Represents a port range expression to be used within a rule. +type Nftables::Port::Range = Pattern[/^\d+-\d+$/]