From 4f2cc4d063680423589bd7ffa0b32b97d520ade6 Mon Sep 17 00:00:00 2001 From: Robin Elfrink Date: Mon, 22 Jun 2020 08:18:56 +0200 Subject: [PATCH 1/3] Add ferm::rule parameters `outerface` and `to_source`. --- manifests/rule.pp | 26 +++++++++++++++++++- spec/defines/rule_spec.rb | 51 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/manifests/rule.pp b/manifests/rule.pp index 1acbfd1..d135a1b 100644 --- a/manifests/rule.pp +++ b/manifests/rule.pp @@ -49,6 +49,8 @@ # @param daddr The destination address we want to match # @param proto_options Optional parameters that will be passed to the protocol (for example to match specific ICMP types) # @param interface an Optional interface where this rule should be applied +# @param outerface an Optional interface via which a packet is going to be sent +# @param to_source Optional new source address of translated packets when using SNAT # @param ensure Set the rule to present or absent # @param table Select the target table (filter/raw/mangle/nat) # Default value: filter @@ -65,6 +67,8 @@ Optional[Variant[Array, String[1]]] $daddr = undef, Optional[String[1]] $proto_options = undef, Optional[String[1]] $interface = undef, + Optional[String[1]] $outerface = undef, + Optional[String[1]] $to_source = undef, Enum['absent','present'] $ensure = 'present', Ferm::Tables $table = 'filter', ){ @@ -80,6 +84,26 @@ fail('Exactly one of "action" or the deprecated "policy" param is required.') } + if $action_temp == 'SNAT' and ($chain != 'POSTROUTING' or $table != 'nat') { + fail('"SNAT" is only valid in the "POSTROUTING" chain of the "nat" table.') + } + + if $outerface and !($chain in ['FORWARD', 'OUTPUT', 'POSTROUTING']) { + fail('Outgoing interface can only be set in the "FORWARD", "OUTPUT" and "POSTROUTING" chains.') + } elsif $outerface { + $outerface_real = "outerface ${outerface}" + } else { + $outerface_real = '' + } + + if $to_source and $action_temp != 'SNAT' { + fail('Setting new source address is only valid with the "SNAT" action.') + } elsif $to_source { + $to_source_real = "to @ipfilter((${$to_source}))" + } else { + $to_source_real = '' + } + if $action_temp in ['RETURN', 'ACCEPT', 'DROP', 'REJECT', 'NOTRACK', 'LOG', 'MARK', 'DNAT', 'SNAT', 'MASQUERADE', 'REDIRECT'] { $action_real = $action_temp @@ -142,7 +166,7 @@ $filename = "${ferm::configdirectory}/chains/${table}-${chain}.conf" } - $rule = squeeze("${comment_real} ${proto_real} ${proto_options_real} ${dport_real} ${sport_real} ${daddr_real} ${saddr_real} ${action_real};", ' ') + $rule = regsubst(squeeze("${comment_real} ${proto_real} ${proto_options_real} ${dport_real} ${sport_real} ${daddr_real} ${saddr_real} ${outerface_real} ${action_real} ${to_source_real}", ' '), '\s?$', ';') if $ensure == 'present' { if $interface { unless defined(Concat::Fragment["${chain}-${interface}-aaa"]) { diff --git a/spec/defines/rule_spec.rb b/spec/defines/rule_spec.rb index 5e4ad69..870f41c 100644 --- a/spec/defines/rule_spec.rb +++ b/spec/defines/rule_spec.rb @@ -41,6 +41,38 @@ it { is_expected.to compile.and_raise_error(%r{Cannot specify both policy and action}) } end + context 'with outerface on input chain' do + let(:title) { 'filter-ssh' } + let :params do + { + chain: 'INPUT', + action: 'ACCEPT', + proto: 'tcp', + dport: '22', + saddr: '127.0.0.1', + outerface: 'eth1' + } + end + + it { is_expected.to compile.and_raise_error(%r{Outgoing interface can only be set in the "FORWARD", "OUTPUT" and "POSTROUTING" chains}) } + end + + context 'with to_source when action is not SNAT' do + let(:title) { 'snat-ssh' } + let :params do + { + chain: 'POSTROUTING', + action: 'ACCEPT', + proto: 'tcp', + dport: '22', + saddr: '127.0.0.1', + to_source: '192.168.1.1' + } + end + + it { is_expected.to compile.and_raise_error(%r{Setting new source address is only valid with the "SNAT" action}) } + end + context 'without a specific interface using legacy policy param' do let(:title) { 'filter-ssh' } let :params do @@ -194,6 +226,25 @@ it { is_expected.to contain_concat__fragment('filter-INPUT-config-include') } it { is_expected.to contain_concat__fragment('filter-SSH-config-include') } end + + context 'source nat with outerface and to_source' do + let(:title) { 'source-nat' } + let :params do + { + chain: 'POSTROUTING', + action: 'SNAT', + proto: 'all', + saddr: '172.16.0.0/24', + outerface: 'eth1', + to_source: '192.168.1.1', + table: 'nat' + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_concat__fragment('POSTROUTING-source-nat').with_content("mod comment comment 'source-nat' proto all saddr @ipfilter((172.16.0.0/24)) outerface eth1 SNAT to @ipfilter((192.168.1.1));\n") } + it { is_expected.to contain_concat__fragment('nat-POSTROUTING-config-include') } + end end end end From ecc8d22d78defd54a7fa6a795cfde70d76f582d5 Mon Sep 17 00:00:00 2001 From: Robin Elfrink Date: Wed, 24 Jun 2020 14:45:30 +0200 Subject: [PATCH 2/3] SNAT is also valid in the INPUT chain. --- manifests/rule.pp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manifests/rule.pp b/manifests/rule.pp index d135a1b..626868a 100644 --- a/manifests/rule.pp +++ b/manifests/rule.pp @@ -84,8 +84,8 @@ fail('Exactly one of "action" or the deprecated "policy" param is required.') } - if $action_temp == 'SNAT' and ($chain != 'POSTROUTING' or $table != 'nat') { - fail('"SNAT" is only valid in the "POSTROUTING" chain of the "nat" table.') + if $action_temp == 'SNAT' and !($chain in ['POSTROUTING', 'INPUT'] and $table == 'nat') { + fail('"SNAT" is only valid in the "POSTROUTING" and "INPUT" chains of the "nat" table.') } if $outerface and !($chain in ['FORWARD', 'OUTPUT', 'POSTROUTING']) { From 9b5e7e36526d3dd9421f83e53df5d2e76d6cc92a Mon Sep 17 00:00:00 2001 From: Robin Elfrink Date: Wed, 24 Jun 2020 15:18:46 +0200 Subject: [PATCH 3/3] Add option `to_destination` for use with DNAT. --- manifests/rule.pp | 18 ++++++++++++++---- spec/defines/rule_spec.rb | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/manifests/rule.pp b/manifests/rule.pp index 626868a..e920bc9 100644 --- a/manifests/rule.pp +++ b/manifests/rule.pp @@ -51,6 +51,7 @@ # @param interface an Optional interface where this rule should be applied # @param outerface an Optional interface via which a packet is going to be sent # @param to_source Optional new source address of translated packets when using SNAT +# @param to_destination Optional new destination address of translated packets when using DNAT # @param ensure Set the rule to present or absent # @param table Select the target table (filter/raw/mangle/nat) # Default value: filter @@ -69,6 +70,7 @@ Optional[String[1]] $interface = undef, Optional[String[1]] $outerface = undef, Optional[String[1]] $to_source = undef, + Optional[String[1]] $to_destination = undef, Enum['absent','present'] $ensure = 'present', Ferm::Tables $table = 'filter', ){ @@ -88,6 +90,10 @@ fail('"SNAT" is only valid in the "POSTROUTING" and "INPUT" chains of the "nat" table.') } + if $action_temp == 'DNAT' and !($chain in ['PREROUTING', 'OUTPUT'] and $table == 'nat') { + fail('"DNAT" is only valid in the "POSTROUTING" and "OUTPUT" chains of the "nat" table.') + } + if $outerface and !($chain in ['FORWARD', 'OUTPUT', 'POSTROUTING']) { fail('Outgoing interface can only be set in the "FORWARD", "OUTPUT" and "POSTROUTING" chains.') } elsif $outerface { @@ -98,10 +104,14 @@ if $to_source and $action_temp != 'SNAT' { fail('Setting new source address is only valid with the "SNAT" action.') - } elsif $to_source { - $to_source_real = "to @ipfilter((${$to_source}))" + } elsif $to_source and $action_temp == 'SNAT' { + $action_options = "to @ipfilter((${$to_source}))" + } elsif $to_destination and $action_temp != 'DNAT' { + fail('Setting new destination address is only valid with the "DNAT" action.') + } elsif $to_destination and $action_temp == 'DNAT' { + $action_options = "to-destination @ipfilter((${$to_destination}))" } else { - $to_source_real = '' + $action_options = '' } if $action_temp in ['RETURN', 'ACCEPT', 'DROP', 'REJECT', 'NOTRACK', 'LOG', @@ -166,7 +176,7 @@ $filename = "${ferm::configdirectory}/chains/${table}-${chain}.conf" } - $rule = regsubst(squeeze("${comment_real} ${proto_real} ${proto_options_real} ${dport_real} ${sport_real} ${daddr_real} ${saddr_real} ${outerface_real} ${action_real} ${to_source_real}", ' '), '\s?$', ';') + $rule = regsubst(squeeze("${comment_real} ${proto_real} ${proto_options_real} ${dport_real} ${sport_real} ${daddr_real} ${saddr_real} ${outerface_real} ${action_real} ${action_options}", ' '), '\s?$', ';') if $ensure == 'present' { if $interface { unless defined(Concat::Fragment["${chain}-${interface}-aaa"]) { diff --git a/spec/defines/rule_spec.rb b/spec/defines/rule_spec.rb index 870f41c..8f512c1 100644 --- a/spec/defines/rule_spec.rb +++ b/spec/defines/rule_spec.rb @@ -73,6 +73,22 @@ it { is_expected.to compile.and_raise_error(%r{Setting new source address is only valid with the "SNAT" action}) } end + context 'with to_destination when action is not DNAT' do + let(:title) { 'dnat-ssh' } + let :params do + { + chain: 'PREROUTING', + action: 'ACCEPT', + proto: 'tcp', + dport: '22', + daddr: '172.16.0.1', + to_destination: '192.168.1.1' + } + end + + it { is_expected.to compile.and_raise_error(%r{Setting new destination address is only valid with the "DNAT" action}) } + end + context 'without a specific interface using legacy policy param' do let(:title) { 'filter-ssh' } let :params do @@ -245,6 +261,24 @@ it { is_expected.to contain_concat__fragment('POSTROUTING-source-nat').with_content("mod comment comment 'source-nat' proto all saddr @ipfilter((172.16.0.0/24)) outerface eth1 SNAT to @ipfilter((192.168.1.1));\n") } it { is_expected.to contain_concat__fragment('nat-POSTROUTING-config-include') } end + + context 'destination nat with to_destination' do + let(:title) { 'destination-nat' } + let :params do + { + chain: 'PREROUTING', + action: 'DNAT', + proto: 'tcp', + daddr: '172.16.0.1', + to_destination: '192.168.1.1', + table: 'nat' + } + end + + it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_concat__fragment('PREROUTING-destination-nat').with_content("mod comment comment 'destination-nat' proto tcp daddr @ipfilter((172.16.0.1)) DNAT to-destination @ipfilter((192.168.1.1));\n") } + it { is_expected.to contain_concat__fragment('nat-PREROUTING-config-include') } + end end end end