diff --git a/manifests/rule.pp b/manifests/rule.pp index f239402..d4bff81 100644 --- a/manifests/rule.pp +++ b/manifests/rule.pp @@ -49,6 +49,9 @@ # @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 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 @@ -65,6 +68,9 @@ 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, + Optional[String[1]] $to_destination = undef, Enum['absent','present'] $ensure = 'present', Ferm::Tables $table = 'filter', ){ @@ -80,6 +86,34 @@ fail('Exactly one of "action" or the deprecated "policy" param is required.') } + 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 $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 { + $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 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 { + $action_options = '' + } + if $action_temp in ['RETURN', 'ACCEPT', 'DROP', 'REJECT', 'NOTRACK', 'LOG', 'MARK', 'DNAT', 'SNAT', 'MASQUERADE', 'REDIRECT'] { $action_real = $action_temp @@ -188,7 +222,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} ${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 f2601c6..3292e03 100644 --- a/spec/defines/rule_spec.rb +++ b/spec/defines/rule_spec.rb @@ -41,6 +41,54 @@ 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 '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 @@ -273,6 +321,43 @@ 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 + + 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