53 changes: 46 additions & 7 deletions lib/puppet/provider/firewall/iptables.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@
hashlimit_htable_max: '--hashlimit-htable-max',
hashlimit_htable_expire: '--hashlimit-htable-expire',
hashlimit_htable_gcinterval: '--hashlimit-htable-gcinterval',
bytecode: '-m bpf --bytecode',
}

# These are known booleans that do not take a value, but we want to munge
Expand Down Expand Up @@ -300,7 +301,7 @@ def munge_resource_map_from_resource(resource_map_original, compare)
:month_days, :week_days, :date_start, :date_stop, :time_contiguous, :kernel_timezone,
:src_cc, :dst_cc, :hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst,
:hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, :hashlimit_htable_size,
:hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval, :name
:hashlimit_htable_max, :hashlimit_htable_expire, :hashlimit_htable_gcinterval, :bytecode, :name
]

def insert
Expand Down Expand Up @@ -393,6 +394,29 @@ def self.rule_to_hash(line, table, counter)
values = values.gsub(%r{-m set --match-set (!\s+)?\S* \S* }, '')
values.insert(ind, "-m set --match-set \"#{sets.join(';')}\" ")
end
# --comment can have multiple values, the same as --match-set
if values =~ %r{-m comment --comment}
ind = values.index('-m comment --comment')
comments = values.scan(%r{-m comment --comment "(.*?[^\\"])"})
comments += values.scan(%r{-m comment --comment ([^"]+?)\b})
values = values.gsub(%r{-m comment --comment (".*?[^\\"]"|[^ ].*)( |$)}, '')
values = values.gsub(%r{-m comment --comment ([^"].*?)[ $]}, '')
values.insert(ind, "-m comment --comment \"#{comments.join(';')}\" ")
end
if values =~ %r{-m addrtype (!\s+)?--src-type}
values = values.gsub(%r{(!\s+)?--src-type (\S*)(\s--limit-iface-(in|out))?}, '--src-type \1\2\3')
ind = values.index('-m addrtype --src-type')
types = values.scan(%r{-m addrtype --src-type ((?:!\s+)?\S*(?: --limit-iface-(?:in|out))?)})
values = values.gsub(%r{-m addrtype --src-type ((?:!\s+)?\S*(?: --limit-iface-(?:in|out))?) ?}, '')
values.insert(ind, "-m addrtype --src-type \"#{types.join(';')}\" ")
end
if values =~ %r{-m addrtype (!\s+)?--dst-type}
values = values.gsub(%r{(!\s+)?--dst-type (\S*)(\s--limit-iface-(in|out))?}, '--dst-type \1\2\3')
ind = values.index('-m addrtype --dst-type')
types = values.scan(%r{-m addrtype --dst-type ((?:!\s+)?\S*(?: --limit-iface-(?:in|out))?)})
values = values.gsub(%r{-m addrtype --dst-type ((?:!\s+)?\S*(?: --limit-iface-(?:in|out))?) ?}, '')
values.insert(ind, "-m addrtype --dst-type \"#{types.join(';')}\" ")
end
# the actual rule will have the ! mark before the option.
values = values.gsub(%r{(!)\s*(-\S+)\s*(\S*)}, '\2 "\1 \3"')
# we do a similar thing for negated address masks (source and destination).
Expand Down Expand Up @@ -502,7 +526,9 @@ def self.rule_to_hash(line, table, counter)
hash[prop] = hash[prop].split(',') unless hash[prop].nil?
end

hash[:ipset] = hash[:ipset].split(';') unless hash[:ipset].nil?
[:ipset, :dst_type, :src_type].each do |prop|
hash[prop] = hash[prop].split(';') unless hash[prop].nil?
end

## clean up DSCP class to HEX mappings
valid_dscp_classes = {
Expand Down Expand Up @@ -561,7 +587,6 @@ def self.rule_to_hash(line, table, counter)
:destination,
:dport,
:dst_range,
:dst_type,
:port,
:physdev_is_bridged,
:physdev_is_in,
Expand All @@ -570,7 +595,6 @@ def self.rule_to_hash(line, table, counter)
:source,
:sport,
:src_range,
:src_type,
:state,
].each do |prop|
if hash[prop] && hash[prop].is_a?(Array)
Expand Down Expand Up @@ -699,6 +723,19 @@ def general_args
raise "#{nflog_feature} is not available on iptables version #{iptables_version}" if resource[nflog_feature] && (iptables_version && iptables_version < '1.3.7')
end

[:dst_type, :src_type].each do |prop|
next unless resource[prop]

resource[prop].each do |type|
if type =~ %r{--limit-iface-(in|out)} && (iptables_version && iptables_version < '1.4.1')
raise '--limit-iface-in and --limit-iface-out are available from iptables version 1.4.1'
end
end

raise "Multiple #{prop} elements are available from iptables version 1.4.1" if resource[prop].length > 1 && (iptables_version && iptables_version < '1.4.1')
raise "#{prop} elements must be unique" if resource[prop].map { |type| type.to_s.gsub(%r{--limit-iface-(in|out)}, '') }.uniq.length != resource[prop].length
end

resource_list.each do |res|
resource_value = nil
if resource[res]
Expand Down Expand Up @@ -727,7 +764,7 @@ def general_args
# ruby 1.8.7 can't .match Symbols ------------------ ^
resource_value = resource_value.to_s.sub!(%r{^!\s*}, '').to_sym
args.insert(-2, '!')
elsif resource_value.is_a?(Array) && res != :ipset
elsif resource_value.is_a?(Array) && ![:ipset, :dst_type, :src_type].include?(res)
should_negate = resource_value.index do |value|
# ruby 1.8.7 can't .match symbols
value.to_s.match(%r{^(!)\s+})
Expand Down Expand Up @@ -760,7 +797,7 @@ def general_args
end

# ipset can accept multiple values with weird iptables arguments
if res == :ipset
if [:ipset, :dst_type, :src_type].include?(res)
resource_value.join(" #{[resource_map[res]].flatten.first} ").split(' ').each do |a|
if a.sub!(%r{^!\s*}, '')
# Negate ipset options
Expand Down Expand Up @@ -839,6 +876,8 @@ def insert_order

# Insert our new or updated rule in the correct order of named rules, but
# offset for unnamed rules.
rules.reject { |r| r.match(unmanaged_rule_regex) }.sort.index(my_rule) + 1 + unnamed_offset
sorted_rules = rules.reject { |r| r.match(unmanaged_rule_regex) }.sort
raise 'Rule sorting error. Make sure that the title of your rule does not start with 9000-9999, as this range is reserved.' if sorted_rules.index(my_rule).nil?
sorted_rules.index(my_rule) + 1 + unnamed_offset
end
end
58 changes: 50 additions & 8 deletions lib/puppet/type/firewall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
feature :queue_num, 'Which NFQUEUE to send packets to'
feature :queue_bypass, 'If nothing is listening on queue_num, allow packets to bypass the queue'
feature :hashlimit, 'Hashlimit features'
feature :bpf, 'Berkeley Paket Filter feature'

# provider specific features
feature :iptables, 'The provider provides iptables features.'
Expand Down Expand Up @@ -330,11 +331,11 @@ def should_to_s(value)
end
end

newproperty(:dst_type, required_features: :address_type) do
newproperty(:dst_type, required_features: :address_type, array_matching: :all) do
desc <<-PUPPETCODE
The destination address type. For example:
dst_type => 'LOCAL'
dst_type => ['LOCAL']
Can be one of:
Expand All @@ -350,19 +351,36 @@ def should_to_s(value)
* THROW - undocumented
* NAT - undocumented
* XRESOLVE - undocumented
In addition, it accepts '--limit-iface-in' and '--limit-iface-out' flags, specified as:
dst_type => ['LOCAL --limit-iface-in']
It can also be negated using '!':
dst_type => ['! LOCAL']
Will accept a single element or an array.
PUPPETCODE

newvalues(*[:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST,
:BLACKHOLE, :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE].map { |address_type|
[address_type, "! #{address_type}".to_sym]
[
address_type,
"! #{address_type}".to_sym,
"#{address_type} --limit-iface-in".to_sym,
"#{address_type} --limit-iface-out".to_sym,
"! #{address_type} --limit-iface-in".to_sym,
"! #{address_type} --limit-iface-out".to_sym,
]
}.flatten)
end

newproperty(:src_type, required_features: :address_type) do
newproperty(:src_type, required_features: :address_type, array_matching: :all) do
desc <<-PUPPETCODE
The source address type. For example:
src_type => 'LOCAL'
src_type => ['LOCAL']
Can be one of:
Expand All @@ -378,11 +396,28 @@ def should_to_s(value)
* THROW - undocumented
* NAT - undocumented
* XRESOLVE - undocumented
In addition, it accepts '--limit-iface-in' and '--limit-iface-out' flags, specified as:
src_type => ['LOCAL --limit-iface-in']
It can also be negated using '!':
src_type => ['! LOCAL']
Will accept a single element or an array.
PUPPETCODE

newvalues(*[:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST,
:BLACKHOLE, :UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE].map { |address_type|
[address_type, "! #{address_type}".to_sym]
[
address_type,
"! #{address_type}".to_sym,
"#{address_type} --limit-iface-in".to_sym,
"#{address_type} --limit-iface-out".to_sym,
"! #{address_type} --limit-iface-in".to_sym,
"! #{address_type} --limit-iface-out".to_sym,
]
}.flatten)
end

Expand Down Expand Up @@ -540,7 +575,7 @@ def should_to_s(value)
iniface => '! lo',
PUPPETCODE
newvalues(%r{^!?\s?[a-zA-Z0-9\-\._\+\:]+$})
newvalues(%r{^!?\s?[a-zA-Z0-9\-\._\+\:@]+$})
end

newproperty(:outiface, required_features: :interface_match) do
Expand All @@ -551,7 +586,7 @@ def should_to_s(value)
outiface => '! lo',
PUPPETCODE
newvalues(%r{^!?\s?[a-zA-Z0-9\-\._\+\:]+$})
newvalues(%r{^!?\s?[a-zA-Z0-9\-\._\+\:@]+$})
end

# NAT specific properties
Expand Down Expand Up @@ -1703,6 +1738,13 @@ def should_to_s(value)
PUPPETCODE
end

newproperty(:bytecode, required_features: :iptables) do
desc <<-PUPPETCODE
Match using Linux Socket Filter. Expects a BPF program in decimal format.
This is the format generated by the nfbpf_compile utility.
PUPPETCODE
end

autorequire(:firewallchain) do
reqs = []
protocol = nil
Expand Down
156 changes: 90 additions & 66 deletions manifests/linux/redhat.pp
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,31 @@
#
# [*ensure_v6*]
# Ensure parameter passed onto Service[] resources.
# Default: running
# Default: undef
#
# [*enable*]
# Enable parameter passed onto Service[] resources.
# Default: true
#
# [*enable_v6*]
# Enable parameter passed onto Service[] resources.
# Default: true
# Default: undef
#
# [*sysconfig_manage*]
# Enable sysconfig configuration for iptables/ip6tables files. This is
# disabled for RedHat 8+ or CentOS 8+
# Default: true
#
class firewall::linux::redhat (
$ensure = running,
$ensure_v6 = undef,
$enable = true,
$enable_v6 = undef,
$service_name = $::firewall::params::service_name,
$service_name_v6 = $::firewall::params::service_name_v6,
$package_name = $::firewall::params::package_name,
$package_ensure = $::firewall::params::package_ensure,
$ensure = running,
$ensure_v6 = undef,
$enable = true,
$enable_v6 = undef,
$service_name = $::firewall::params::service_name,
$service_name_v6 = $::firewall::params::service_name_v6,
$package_name = $::firewall::params::package_name,
$package_ensure = $::firewall::params::package_ensure,
$sysconfig_manage = $::firewall::params::sysconfig_manage,
) inherits ::firewall::params {
$_ensure_v6 = pick($ensure_v6, $ensure)
$_enable_v6 = pick($enable_v6, $enable)
Expand All @@ -47,6 +52,12 @@
}
}

# in RHEL 8 / CentOS 8 nftables provides a replacement iptables cli
# but there is no nftables specific for ipv6 so throw a warning
if !$service_name_v6 and ($ensure_v6 or $enable_v6) {
warning('No v6 service available, $ensure_v6 and $enable_v6 are ignored')
}

if $package_name {
package { $package_name:
ensure => $package_ensure,
Expand All @@ -67,88 +78,101 @@
}
}

if ($::operatingsystem == 'Amazon') and (versioncmp($::operatingsystemmajrelease, '4') >= 0) {
if ($::operatingsystem == 'Amazon') and (versioncmp($::operatingsystemmajrelease, '4') >= 0)
or ($::operatingsystem == 'Amazon') and (versioncmp($::operatingsystemmajrelease, '2') >= 0) {
service { $service_name:
ensure => $ensure,
enable => $enable,
hasstatus => true,
provider => systemd,
}
service { $service_name_v6:
ensure => $_ensure_v6,
enable => $_enable_v6,
hasstatus => true,
provider => systemd,
if $service_name_v6 {
service { $service_name_v6:
ensure => $_ensure_v6,
enable => $_enable_v6,
hasstatus => true,
provider => systemd,
}
}
} else {
service { $service_name:
ensure => $ensure,
enable => $enable,
hasstatus => true,
}
service { $service_name_v6:
ensure => $_ensure_v6,
enable => $_enable_v6,
hasstatus => true,
if $service_name_v6 {
service { $service_name_v6:
ensure => $_ensure_v6,
enable => $_enable_v6,
hasstatus => true,
}
}
}

file { "/etc/sysconfig/${service_name}":
ensure => present,
owner => 'root',
group => 'root',
mode => '0600',
}
file { "/etc/sysconfig/${service_name_v6}":
ensure => present,
owner => 'root',
group => 'root',
mode => '0600',
}
if $sysconfig_manage {
file { "/etc/sysconfig/${service_name}":
ensure => present,
owner => 'root',
group => 'root',
mode => '0600',
}
if $service_name_v6 {
file { "/etc/sysconfig/${service_name_v6}":
ensure => present,
owner => 'root',
group => 'root',
mode => '0600',
}
}

# Before puppet 4, the autobefore on the firewall type does not work - therefore
# we need to keep this workaround here
if versioncmp($::puppetversion, '4.0') <= 0 {
File["/etc/sysconfig/${service_name}"] -> Service[$service_name]
File["/etc/sysconfig/${service_name_v6}"] -> Service[$service_name_v6]
}
# Before puppet 4, the autobefore on the firewall type does not work - therefore
# we need to keep this workaround here
if versioncmp($::puppetversion, '4.0') <= 0 {
File<| title == "/etc/sysconfig/${service_name}" |> -> Service<| title == $service_name |>
File<| title == "/etc/sysconfig/${service_name_v6}" |> -> Service<| title == $service_name_v6 |>
}

# Redhat 7 selinux user context for /etc/sysconfig/iptables is set to system_u
# Redhat 7 selinux type context for /etc/sysconfig/iptables is set to system_conf_t
case $::selinux {
#lint:ignore:quoted_booleans
'true',true: {
case $::operatingsystem {
'CentOS': {
case $::operatingsystemrelease {
/^6\..*/: {
File["/etc/sysconfig/${service_name}"] { seluser => 'unconfined_u', seltype => 'system_conf_t' }
File["/etc/sysconfig/${service_name_v6}"] { seluser => 'unconfined_u', seltype => 'system_conf_t' }
}
# Redhat 7 selinux user context for /etc/sysconfig/iptables is set to system_u
# Redhat 7 selinux type context for /etc/sysconfig/iptables is set to system_conf_t
case $::selinux {
#lint:ignore:quoted_booleans
'true',true: {
case $::operatingsystem {
'CentOS': {
case $::operatingsystemrelease {
/^6\..*/: {
$seluser = 'unconfined_u'
$seltype = 'system_conf_t'
}

/^7\..*/: {
File["/etc/sysconfig/${service_name}"] { seluser => 'system_u', seltype => 'system_conf_t' }
File["/etc/sysconfig/${service_name_v6}"] { seluser => 'system_u', seltype => 'system_conf_t' }
}
/^7\..*/: {
$seluser = 'system_u'
$seltype = 'system_conf_t'
}

default : {
File["/etc/sysconfig/${service_name}"] { seluser => 'unconfined_u', seltype => 'etc_t' }
File["/etc/sysconfig/${service_name_v6}"] { seluser => 'unconfined_u', seltype => 'etc_t' }
default : {
$seluser = 'unconfined_u'
$seltype = 'etc_t'
}
}
File<| title == "/etc/sysconfig/${service_name}" |> { seluser => $seluser, seltype => $seltype }
File<| title == "/etc/sysconfig/${service_name_v6}" |> { seluser => $seluser, seltype => $seltype }
}
}

# Fedora uses the same SELinux context as Redhat
'Fedora': {
File["/etc/sysconfig/${service_name}"] { seluser => 'system_u', seltype => 'system_conf_t' }
File["/etc/sysconfig/${service_name_v6}"] { seluser => 'system_u', seltype => 'system_conf_t' }
}
# Fedora uses the same SELinux context as Redhat
'Fedora': {
$seluser = 'system_u'
$seltype = 'system_conf_t'
File<| title == "/etc/sysconfig/${service_name}" |> { seluser => $seluser, seltype => $seltype }
File<| title == "/etc/sysconfig/${service_name_v6}" |> { seluser => $seluser, seltype => $seltype }
}

default: {}
default: {}

}
}
default: {}
#lint:endignore
}
default: {}
#lint:endignore
}
}
21 changes: 18 additions & 3 deletions manifests/params.pp
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,39 @@
$package_ensure = 'present'
case $::osfamily {
'RedHat': {
$service_name = 'iptables'
$service_name_v6 = 'ip6tables'
case $::operatingsystem {
'Amazon': {
$service_name = 'iptables'
$service_name_v6 = 'ip6tables'
$package_name = undef
$sysconfig_manage = true
}
'Fedora': {
$service_name = 'iptables'
$service_name_v6 = 'ip6tables'
if versioncmp($::operatingsystemrelease, '15') >= 0 {
$package_name = 'iptables-services'
} else {
$package_name = undef
}
$sysconfig_manage = true
}
default: {
if versioncmp($::operatingsystemrelease, '7.0') >= 0 {
if versioncmp($::operatingsystemrelease, '8.0') >= 0 {
$service_name = 'nftables'
$service_name_v6 = undef
$package_name = 'nftables'
$sysconfig_manage = false
} elsif versioncmp($::operatingsystemrelease, '7.0') >= 0 {
$service_name = 'iptables'
$service_name_v6 = 'ip6tables'
$package_name = 'iptables-services'
$sysconfig_manage = true
} else {
$service_name = 'iptables'
$service_name_v6 = 'ip6tables'
$package_name = 'iptables-ipv6'
$sysconfig_manage = true
}
}
}
Expand Down
17 changes: 6 additions & 11 deletions metadata.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "puppetlabs-firewall",
"version": "1.14.0",
"version": "1.15.0",
"author": "puppetlabs",
"summary": "Manages Firewalls such as iptables",
"license": "Apache-2.0",
Expand Down Expand Up @@ -48,7 +48,8 @@
"operatingsystem": "SLES",
"operatingsystemrelease": [
"11 SP1",
"12"
"12",
"15"
]
},
{
Expand All @@ -65,12 +66,6 @@
"16.04",
"18.04"
]
},
{
"operatingsystem": "Gentoo",
"operatingsystemrelease": [
"1.0"
]
}
],
"requirements": [
Expand All @@ -79,7 +74,7 @@
"version_requirement": ">= 4.7.0 < 7.0.0"
}
],
"template-url": "https://github.com/puppetlabs/pdk-templates",
"template-ref": "heads/master-0-g8fc95db",
"pdk-version": "1.7.0"
"template-url": "https://github.com/puppetlabs/pdk-templates/",
"template-ref": "heads/master-0-g6814a87",
"pdk-version": "1.7.1"
}
31 changes: 31 additions & 0 deletions spec/acceptance/bytecode_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
require 'spec_helper_acceptance'

# --bytecode is only supported by operatingsystems using nftables (in general Linux kernel 3.13, RedHat 7 (and derivates) with 3.10)
# Skipping those from which we know they would fail.
describe 'bytecode property', unless: (fact('osfamily') == 'RedHat' && fact('operatingsystemmajrelease') < '7') ||
(fact('operatingsystem') == 'OracleLinux' && fact('operatingsystemmajrelease') <= '7') ||
(fact('operatingsystem') == 'SLES' && fact('operatingsystemmajrelease') <= '11') do
describe 'bytecode' do
context '4,48 0 0 9,21 0 1 6,6 0 0 1,6 0 0 0' do
pp = <<-PUPPETCODE
class { '::firewall': }
firewall { '102 - test':
action => 'accept',
bytecode => '4,48 0 0 9,21 0 1 6,6 0 0 1,6 0 0 0',
chain => 'OUTPUT',
proto => 'all',
table => 'filter',
}
PUPPETCODE
it 'applies' do
apply_manifest(pp, catch_failures: true)
end

it 'contains the rule' do
shell('iptables-save') do |r|
expect(r.stdout).to match(%r{-A OUTPUT -m bpf --bytecode "4,48 0 0 9,21 0 1 6,6 0 0 1,6 0 0 0" -m comment --comment "102 - test" -j ACCEPT})
end
end
end
end
end
237 changes: 237 additions & 0 deletions spec/acceptance/firewall_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ class { '::firewall': }
end
end
end

context 'when invalid ordering range specified' do
pp = <<-PUPPETCODE
class { '::firewall': }
firewall { '9946 test': ensure => present }
PUPPETCODE
it 'fails' do
apply_manifest(pp, expect_failures: true) do |r|
expect(r.stderr).to match(%r{Rule sorting error})
end
end
end
end

describe 'ensure' do
Expand Down Expand Up @@ -586,6 +598,117 @@ class { '::firewall': }
end
end
end

context 'when LOCAL --limit-iface-in', unless: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp97 = <<-PUPPETCODE
class { '::firewall': }
firewall { '613 - test':
proto => tcp,
action => accept,
#{type} => 'LOCAL --limit-iface-in',
}
PUPPETCODE
it 'applies' do
apply_manifest(pp97, catch_failures: true)
end

it 'contains the rule' do
shell('iptables-save') do |r|
expect(r.stdout).to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL --limit-iface-in -m comment --comment "613 - test" -j ACCEPT})
end
end
end

context 'when LOCAL --limit-iface-in fail', if: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp98 = <<-PUPPETCODE
class { '::firewall': }
firewall { '614 - test':
proto => tcp,
action => accept,
#{type} => 'LOCAL --limit-iface-in',
}
PUPPETCODE
it 'fails' do
apply_manifest(pp98, expect_failures: true) do |r|
expect(r.stderr).to match(%r{--limit-iface-in and --limit-iface-out are available from iptables version})
end
end

it 'does not contain the rule' do
shell('iptables-save') do |r|
expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL --limit-iface-in -m comment --comment "614 - test" -j ACCEPT})
end
end
end

context 'when duplicated LOCAL', unless: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp99 = <<-PUPPETCODE
class { '::firewall': }
firewall { '615 - test':
proto => tcp,
action => accept,
#{type} => ['LOCAL', 'LOCAL'],
}
PUPPETCODE
it 'fails' do
apply_manifest(pp99, expect_failures: true) do |r|
expect(r.stderr).to match(%r{#{type} elements must be unique})
end
end

it 'does not contain the rule' do
shell('iptables-save') do |r|
expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype --#{type.tr('_', '-')} LOCAL -m comment --comment "615 - test" -j ACCEPT})
end
end
end

context 'when multiple addrtype', unless: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp100 = <<-PUPPETCODE
class { '::firewall': }
firewall { '616 - test':
proto => tcp,
action => accept,
#{type} => ['LOCAL', '! LOCAL'],
}
PUPPETCODE
it 'applies' do
apply_manifest(pp100, catch_failures: true)
end

it 'contains the rule' do
shell('iptables-save') do |r|
expect(r.stdout).to match(%r{-A INPUT -p tcp -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype ! --#{type.tr('_', '-')} LOCAL -m comment --comment "616 - test" -j ACCEPT})
end
end
end

context 'when multiple addrtype fail', if: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp101 = <<-PUPPETCODE
class { '::firewall': }
firewall { '616 - test':
proto => tcp,
action => accept,
#{type} => ['LOCAL', '! LOCAL'],
}
PUPPETCODE
it 'fails' do
apply_manifest(pp101, expect_failures: true) do |r|
expect(r.stderr).to match(%r{Multiple #{type} elements are available from iptables version})
end
end

it 'does not contain the rule' do
shell('iptables-save') do |r|
expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype ! --#{type.tr('_', '-')} LOCAL -m comment --comment "616 - test" -j ACCEPT})
end
end
end
end
end

Expand Down Expand Up @@ -1574,6 +1697,120 @@ class { '::firewall': }
end
end
end

context 'when LOCAL --limit-iface-in', unless: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp102 = <<-PUPPETCODE
class { '::firewall': }
firewall { '617 - test':
proto => tcp,
action => accept,
#{type} => 'LOCAL --limit-iface-in',
}
PUPPETCODE
it 'applies' do
apply_manifest(pp102, catch_failures: true)
end

it 'contains the rule' do
shell('iptables-save') do |r|
expect(r.stdout).to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL --limit-iface-in -m comment --comment "617 - test" -j ACCEPT})
end
end
end

context 'when LOCAL --limit-iface-in fail', if: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp103 = <<-PUPPETCODE
class { '::firewall': }
firewall { '618 - test':
proto => tcp,
action => accept,
#{type} => 'LOCAL --limit-iface-in',
}
PUPPETCODE
it 'fails' do
apply_manifest(pp103, expect_failures: true) do |r|
expect(r.stderr).to match(%r{--limit-iface-in and --limit-iface-out are available from iptables version})
end
end

it 'does not contain the rule' do
shell('iptables-save') do |r|
expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL --limit-iface-in -m comment --comment "618 - test" -j ACCEPT})
end
end
end

context 'when duplicated LOCAL', unless: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp104 = <<-PUPPETCODE
class { '::firewall': }
firewall { '619 - test':
proto => tcp,
action => accept,
#{type} => ['LOCAL', 'LOCAL'],
provider => 'ip6tables',
}
PUPPETCODE
it 'fails' do
apply_manifest(pp104, expect_failures: true) do |r|
expect(r.stderr).to match(%r{#{type} elements must be unique})
end
end

it 'does not contain the rule' do
shell('ip6tables-save') do |r|
expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype\s.*\sLOCAL -m addrtype\s.*\sLOCAL -m comment --comment "619 - test" -j ACCEPT})
end
end
end

context 'when multiple addrtype', unless: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp105 = <<-PUPPETCODE
class { '::firewall': }
firewall { '620 - test':
proto => tcp,
action => accept,
#{type} => ['LOCAL', '! LOCAL'],
provider => 'ip6tables',
}
PUPPETCODE
it 'applies' do
apply_manifest(pp105, catch_failures: true)
end

it 'contains the rule' do
shell('ip6tables-save') do |r|
expect(r.stdout).to match(%r{-A INPUT -p tcp -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype ! --#{type.tr('_', '-')} LOCAL -m comment --comment "620 - test" -j ACCEPT})
end
end
end

context 'when multiple addrtype fail', if: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
pp106 = <<-PUPPETCODE
class { '::firewall': }
firewall { '616 - test':
proto => tcp,
action => accept,
#{type} => ['LOCAL', '! LOCAL'],
provider => 'ip6tables',
}
PUPPETCODE
it 'fails' do
apply_manifest(pp106, expect_failures: true) do |r|
expect(r.stderr).to match(%r{Multiple #{type} elements are available from iptables version})
end
end

it 'does not contain the rule' do
shell('ip6tables-save') do |r|
expect(r.stdout).not_to match(%r{-A INPUT -p tcp -m addrtype --#{type.tr('_', '-')} LOCAL -m addrtype ! --#{type.tr('_', '-')} LOCAL -m comment --comment "616 - test" -j ACCEPT})
end
end
end
end
end
end
Expand Down
16 changes: 16 additions & 0 deletions spec/acceptance/resource_cmd_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,22 @@
end
end

context 'when accepts rules with multiple comments', unless: (fact('operatingsystem') == 'RedHat' && fact('operatingsystemmajrelease') <= '5') ||
(fact('operatingsystem') == 'CentOS' && fact('operatingsystemmajrelease') <= '5') do
before(:all) do
iptables_flush_all_tables
shell('iptables -A INPUT -j ACCEPT -p tcp --dport 80 -m comment --comment "http" -m comment --comment "http"')
end

it do
shell('puppet resource firewall') do |r|
r.exit_code.should be_zero
# don't check stdout, testing preexisting rules, output is normal
r.stderr.should be_empty
end
end
end

context 'when accepts rules with negation' do
before :all do
iptables_flush_all_tables
Expand Down
1 change: 0 additions & 1 deletion spec/default_facts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#
# Facts specified here will override the values provided by rspec-puppet-facts.
---
concat_basedir: ""
ipaddress: "172.16.254.254"
is_pe: false
macaddress: "AA:AA:AA:AA:AA:AA"
95 changes: 91 additions & 4 deletions spec/fixtures/iptables/conversion_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,14 @@
line: '-A INPUT -m addrtype --dst-type LOCAL',
table: 'filter',
params: {
dst_type: 'LOCAL',
dst_type: ['LOCAL'],
},
},
'src_type_1' => {
line: '-A INPUT -m addrtype --src-type LOCAL',
table: 'filter',
params: {
src_type: 'LOCAL',
src_type: ['LOCAL'],
},
},
'dst_range_1' => {
Expand Down Expand Up @@ -240,6 +240,20 @@
source: '192.168.0.1/32',
},
},
'multiple_comments' => {
line: '-A INPUT -s 192.168.0.1/32 -m comment --comment "000 allow from 192.168.0.1, please" -m comment --comment "another comment"',
table: 'filter',
params: {
name: '000 allow from 192.168.0.1, please;another comment',
},
},
'comments_without_quotes' => {
line: '-A INPUT -s 192.168.0.1/32 -m comment --comment comment_without_quotes',
table: 'filter',
params: {
name: '9000 comment_without_quotes',
},
},
'string_escape_sequences' => {
line: '-A INPUT -m comment --comment "000 parse escaped \\"s, \\\'s, and \\\\s"',
table: 'filter',
Expand Down Expand Up @@ -356,6 +370,39 @@
action: 'drop',
},
},
'addrtype_limit_iface_out' => {
line: '-A cali-POSTROUTING -o tunl0 -m comment --comment "000 cali:JHlpT-eSqR1TvyYm" -m addrtype --src-type LOCAL --limit-iface-out -j MASQUERADE',
table: 'filter',
params: {
chain: 'cali-POSTROUTING',
outiface: 'tunl0',
name: '000 cali:JHlpT-eSqR1TvyYm',
jump: 'MASQUERADE',
src_type: ['LOCAL --limit-iface-out'],
},
},
'addrtype_negated' => {
line: '-A cali-POSTROUTING -o tunl0 -m comment --comment "000 cali:JHlpT-eSqR1TvyYm" -m addrtype ! --src-type LOCAL -j MASQUERADE',
table: 'filter',
params: {
chain: 'cali-POSTROUTING',
outiface: 'tunl0',
name: '000 cali:JHlpT-eSqR1TvyYm',
jump: 'MASQUERADE',
src_type: ['! LOCAL'],
},
},
'addrtype_multiple' => {
line: '-A cali-POSTROUTING -o tunl0 -m comment --comment "000 cali:JHlpT-eSqR1TvyYm" -m addrtype ! --src-type LOCAL --limit-iface-out -m addrtype --src-type LOCAL -j MASQUERADE',
table: 'filter',
params: {
chain: 'cali-POSTROUTING',
outiface: 'tunl0',
name: '000 cali:JHlpT-eSqR1TvyYm',
jump: 'MASQUERADE',
src_type: ['! LOCAL --limit-iface-out', 'LOCAL'],
},
},
'iniface_1_negated' => {
line: '-A INPUT ! -i eth0 -j DROP -m comment --comment "060 iniface"',
table: 'filter',
Expand Down Expand Up @@ -824,15 +871,55 @@
table: 'filter',
dst_type: 'LOCAL',
},
args: ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--dst-type', :LOCAL, '-m', 'comment', '--comment', '000 dst_type'],
args: ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--dst-type', 'LOCAL', '-m', 'comment', '--comment', '000 dst_type'],
},
'dst_type_as_array' => {
params: {
name: '000 dst_type',
table: 'filter',
dst_type: ['LOCAL'],
},
args: ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--dst-type', 'LOCAL', '-m', 'comment', '--comment', '000 dst_type'],
},
'dst_type_multiple' => {
params: {
name: '000 dst_type',
table: 'filter',
dst_type: ['LOCAL', '! LOCAL'],
},
args: ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--dst-type', 'LOCAL', '-m', 'addrtype', '!', '--dst-type', 'LOCAL', '-m', 'comment', '--comment', '000 dst_type'],
},
'dst_type_limit' => {
params: {
name: '000 dst_type',
table: 'filter',
dst_type: ['LOCAL --limit-iface-in'],
},
args: ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--dst-type', 'LOCAL', '--limit-iface-in', '-m', 'comment', '--comment', '000 dst_type'],
},
'src_type_1' => {
params: {
name: '000 src_type',
table: 'filter',
src_type: 'LOCAL',
},
args: ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--src-type', :LOCAL, '-m', 'comment', '--comment', '000 src_type'],
args: ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--src-type', 'LOCAL', '-m', 'comment', '--comment', '000 src_type'],
},
'src_type_as_array' => {
params: {
name: '000 src_type',
table: 'filter',
src_type: ['LOCAL'],
},
args: ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--src-type', 'LOCAL', '-m', 'comment', '--comment', '000 src_type'],
},
'src_type_multiple' => {
params: {
name: '000 src_type',
table: 'filter',
src_type: ['LOCAL', '! LOCAL'],
},
args: ['-t', :filter, '-p', :tcp, '-m', 'addrtype', '--src-type', 'LOCAL', '-m', 'addrtype', '!', '--src-type', 'LOCAL', '-m', 'comment', '--comment', '000 src_type'],
},
'dst_range_1' => {
params: {
Expand Down
28 changes: 14 additions & 14 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
require 'puppetlabs_spec_helper/module_spec_helper'
require 'rspec-puppet-facts'

begin
require 'spec_helper_local' if File.file?(File.join(File.dirname(__FILE__), 'spec_helper_local.rb'))
rescue LoadError => loaderror
warn "Could not require spec_helper_local: #{loaderror.message}"
end
require 'spec_helper_local' if File.file?(File.join(File.dirname(__FILE__), 'spec_helper_local.rb'))

include RspecPuppetFacts

Expand All @@ -14,15 +10,19 @@
facterversion: Facter.version,
}

default_facts_path = File.expand_path(File.join(File.dirname(__FILE__), 'default_facts.yml'))
default_module_facts_path = File.expand_path(File.join(File.dirname(__FILE__), 'default_module_facts.yml'))
default_fact_files = [
File.expand_path(File.join(File.dirname(__FILE__), 'default_facts.yml')),
File.expand_path(File.join(File.dirname(__FILE__), 'default_module_facts.yml')),
]

if File.exist?(default_facts_path) && File.readable?(default_facts_path)
default_facts.merge!(YAML.safe_load(File.read(default_facts_path)))
end
default_fact_files.each do |f|
next unless File.exist?(f) && File.readable?(f) && File.size?(f)

if File.exist?(default_module_facts_path) && File.readable?(default_module_facts_path)
default_facts.merge!(YAML.safe_load(File.read(default_module_facts_path)))
begin
default_facts.merge!(YAML.safe_load(File.read(f)))
rescue => e
RSpec.configuration.reporter.message "WARNING: Unable to load #{f}: #{e}"
end
end

RSpec.configure do |c|
Expand All @@ -36,8 +36,8 @@

def ensure_module_defined(module_name)
module_name.split('::').reduce(Object) do |last_module, next_module|
last_module.const_set(next_module, Module.new) unless last_module.const_defined?(next_module)
last_module.const_get(next_module)
last_module.const_set(next_module, Module.new) unless last_module.const_defined?(next_module, false)
last_module.const_get(next_module, false)
end
end

Expand Down
70 changes: 70 additions & 0 deletions spec/unit/classes/firewall_linux_redhat_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
['RedHat', 'CentOS', 'Fedora'].each do |os|
oldreleases = ((os == 'Fedora') ? ['14'] : ['6.5'])
newreleases = ((os == 'Fedora') ? ['15', 'Rawhide'] : ['7.0.1406'])
nftablesreleases = ((os == 'Fedora') ? [] : ['8.0'])

oldreleases.each do |osrel|
context "os #{os} and osrel #{osrel}" do
Expand All @@ -50,6 +51,10 @@

it { is_expected.not_to contain_service('firewalld') }
it { is_expected.not_to contain_package('iptables-services') }
it {
is_expected.to contain_file('/etc/sysconfig/iptables')
is_expected.to contain_file('/etc/sysconfig/ip6tables')
}

it_behaves_like 'ensures iptables service'
end
Expand Down Expand Up @@ -79,6 +84,10 @@
enable: 'true',
)
}
it {
is_expected.to contain_file('/etc/sysconfig/iptables')
is_expected.to contain_file('/etc/sysconfig/ip6tables')
}

context 'with ensure => stopped' do
let(:params) { { ensure: 'stopped' } }
Expand Down Expand Up @@ -138,5 +147,66 @@
it_behaves_like 'ensures iptables service'
end
end

nftablesreleases.each do |osrel|
context "os #{os} and osrel #{osrel}" do
let(:facts) do
{
operatingsystem: os,
operatingsystemrelease: osrel,
osfamily: 'RedHat',
selinux: false,
puppetversion: Puppet.version,
}
end

it {
is_expected.to contain_service('nftables').with(
ensure: 'running',
enable: 'true',
)
is_expected.not_to contain_service('iptables')
}

context 'with ensure => stopped' do
let(:params) { { ensure: 'stopped' } }

it {
is_expected.to contain_service('nftables').with(
ensure: 'stopped',
)
}
end

context 'with enable => false' do
let(:params) { { enable: 'false' } }

it {
is_expected.to contain_service('nftables').with(
enable: 'false',
)
}
end

it {
is_expected.to contain_service('firewalld').with(
ensure: 'stopped',
enable: false,
before: ['Package[nftables]', 'Service[nftables]'],
)
}

it {
is_expected.to contain_package('nftables').with(
ensure: 'present',
before: 'Service[nftables]',
)
}

it {
is_expected.not_to contain_file('/etc/sysconfig/nftables')
}
end
end
end
end
10 changes: 7 additions & 3 deletions spec/unit/puppet/type/firewall_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -198,9 +198,13 @@

[:UNSPEC, :UNICAST, :LOCAL, :BROADCAST, :ANYCAST, :MULTICAST, :BLACKHOLE,
:UNREACHABLE, :PROHIBIT, :THROW, :NAT, :XRESOLVE].each do |type|
it "should accept #{addrtype} value #{type}" do
resource[addrtype] = type
expect(resource[addrtype]).to eql type
['! ', ''].each do |negation|
['', ' --limit-iface-in', ' --limit-iface-out'].each do |limit|
it "should accept #{addrtype} value #{negation}#{type}#{limit}" do
resource[addrtype] = type
expect(resource[addrtype]).to eql [type]
end
end
end
end

Expand Down