484 changes: 32 additions & 452 deletions .rubocop_todo.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ appveyor.yml:
Gemfile:
optional:
":development":
- gem: 'puppet-resource_api'
- gem: github_changelog_generator
version: '= 1.15.2'
Rakefile:
Expand Down
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org).

## [v7.0.0](https://github.com/puppetlabs/puppetlabs-firewall/tree/v7.0.0) - 2023-09-13

[Full Changelog](https://github.com/puppetlabs/puppetlabs-firewall/compare/v6.0.0...v7.0.0)

### Changed
- (CAT-376) Rework firewall module to use the resource_api [#1145](https://github.com/puppetlabs/puppetlabs-firewall/pull/1145) ([david22swan](https://github.com/david22swan))

### Fixed

- (maint) Update all README.md mentions of `action` to `jump` [#1151](https://github.com/puppetlabs/puppetlabs-firewall/pull/1151) ([david22swan](https://github.com/david22swan))
- (RUBOCOP) Resolve Rubocop Issues [#1149](https://github.com/puppetlabs/puppetlabs-firewall/pull/1149) ([david22swan](https://github.com/david22swan))

## [v6.0.0](https://github.com/puppetlabs/puppetlabs-firewall/tree/v6.0.0) - 2023-07-25

[Full Changelog](https://github.com/puppetlabs/puppetlabs-firewall/compare/v5.0.0...v6.0.0)
Expand Down Expand Up @@ -467,7 +479,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
- Fixes SELinux compatibility with EL6 [#664](https://github.com/puppetlabs/puppetlabs-firewall/pull/664) ([bmjen](https://github.com/bmjen))
- Re-add RHEL7 SELinux support for puppet3 [#660](https://github.com/puppetlabs/puppetlabs-firewall/pull/660) ([bmjen](https://github.com/bmjen))
- Fixing issue with double quotes being removed when part of the comment [#646](https://github.com/puppetlabs/puppetlabs-firewall/pull/646) ([kindred](https://github.com/kindred))
- Implemented paramters for NFQUEUE jump target [#644](https://github.com/puppetlabs/puppetlabs-firewall/pull/644) ([ianand0204](https://github.com/ianand0204))
- Implemented paramters for NFQUEUE jump target [#644](https://github.com/puppetlabs/puppetlabs-firewall/pull/644) ([pid1co](https://github.com/pid1co))
- (MODULES-3572) Ip6tables service is not managed in the redhat family. [#641](https://github.com/puppetlabs/puppetlabs-firewall/pull/641) ([marcofl](https://github.com/marcofl))

## [1.8.1](https://github.com/puppetlabs/puppetlabs-firewall/tree/1.8.1) - 2016-05-17
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ group :development do
gem "rubocop-performance", '= 1.16.0', require: false
gem "rubocop-rspec", '= 2.19.0', require: false
gem "rb-readline", '= 0.5.5', require: false, platforms: [:mswin, :mingw, :x64_mingw]
gem "puppet-resource_api", require: false
gem "github_changelog_generator", '= 1.15.2', require: false
end
group :system_tests do
Expand Down
118 changes: 90 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,24 @@ class my_fw::pre {
# Default firewall rules
firewall { '000 accept all icmp':
proto => 'icmp',
action => 'accept',
proto => 'icmp',
jump => 'accept',
}
-> firewall { '001 accept all to lo interface':
proto => 'all',
iniface => 'lo',
action => 'accept',
jump => 'accept',
}
-> firewall { '002 reject local traffic not on loopback interface':
iniface => '! lo',
proto => 'all',
destination => '127.0.0.1/8',
action => 'reject',
jump => 'reject',
}
-> firewall { '003 accept related established rules':
proto => 'all',
state => ['RELATED', 'ESTABLISHED'],
action => 'accept',
jump => 'accept',
}
}
```
Expand All @@ -108,7 +108,7 @@ existing connections are not closed.
class my_fw::post {
firewall { '999 drop all':
proto => 'all',
action => 'drop',
jump => 'drop',
before => undef,
}
}
Expand Down Expand Up @@ -170,15 +170,7 @@ resources { 'firewallchain':
Internal chains can not be deleted. In order to avoid all the confusing
Warning/Notice messages when using `purge => true`, like these ones:

Notice: Compiled catalog for blonde-height.delivery.puppetlabs.net in environment production in 0.05 seconds
Warning: Firewallchain[INPUT:mangle:IPv4](provider=iptables_chain): Attempting to destroy internal chain INPUT:mangle:IPv4
Notice: /Stage[main]/Main/Firewallchain[INPUT:mangle:IPv4]/ensure: removed
Warning: Firewallchain[FORWARD:mangle:IPv4](provider=iptables_chain): Attempting to destroy internal chain FORWARD:mangle:IPv4
Notice: /Stage[main]/Main/Firewallchain[FORWARD:mangle:IPv4]/ensure: removed
Warning: Firewallchain[OUTPUT:mangle:IPv4](provider=iptables_chain): Attempting to destroy internal chain OUTPUT:mangle:IPv4
Notice: /Stage[main]/Main/Firewallchain[OUTPUT:mangle:IPv4]/ensure: removed
Warning: Firewallchain[POSTROUTING:mangle:IPv4](provider=iptables_chain): Attempting to destroy internal chain POSTROUTING:mangle:IPv4
Notice: /Stage[main]/Main/Firewallchain[POSTROUTING:mangle:IPv4]/ensure: removed
Warning: Inbuilt Chains may not be deleted. Chain `POSTROUTING:mangle:IPv6` will be flushed and have it's policy reverted to default.

Please create firewallchains for every internal chain. Here is an example:

Expand Down Expand Up @@ -226,16 +218,16 @@ Basic accept ICMP request example:

```puppet
firewall { '000 accept all icmp requests':
proto => 'icmp',
action => 'accept',
proto => 'icmp',
jump => 'accept',
}
```

Drop all:

```puppet
firewall { '999 drop all other requests':
action => 'drop',
jump => 'drop',
}
```

Expand All @@ -247,8 +239,8 @@ IPv6 rules can be specified using the _ip6tables_ provider:
firewall { '006 Allow inbound SSH (v6)':
dport => 22,
proto => 'tcp',
action => 'accept',
provider => 'ip6tables',
jump => 'accept',
protocol => 'ip6tables',
}
```

Expand All @@ -273,36 +265,50 @@ class profile::apache {
firewall { '100 allow http and https access':
dport => [80, 443],
proto => 'tcp',
action => 'accept',
jump => 'accept',
}
}
```

### Rule inversion

Firewall rules may be inverted by prefixing the value of a parameter by "! ". If the value is an array, then every item in the array must be prefixed as iptables does not understand inverting a single value.
Firewall rules may be inverted by prefixing the value of a parameter by "! ".

Parameters that understand inversion are: connmark, ctstate, destination, dport, dst\_range, dst\_type, iniface, outiface, port, proto, source, sport, src\_range and src\_type.

If the value is an array, then either the first value of the array, or all of its values must be prefixed in order to invert them all.
For most array attributes it is not possible to invert only one passed value.

Examples:

```puppet
firewall { '001 disallow esp protocol':
action => 'accept',
jump => 'accept',
proto => '! esp',
}
firewall { '002 drop NEW external website packets with FIN/RST/ACK set and SYN unset':
chain => 'INPUT',
state => 'NEW',
action => 'drop',
jump => 'drop',
proto => 'tcp',
sport => ['! http', '! 443'],
sport => ['! http', '443'],
source => '! 10.0.0.0/8',
tcp_flags => '! FIN,SYN,RST,ACK SYN',
}
```

There are exceptions to this however, with attributes such as src\_type, dst\_type and ipset allowing the user to negate each passed values seperately.

Examples:

```puppet
firewall { '001 allow local disallow anycast':
jump => 'accept',
src_type => ['LOCAL', '! ANYCAST'],
}
```

### Additional uses for the firewall module

You can apply firewall rules to specific nodes. Usually, you should put the firewall rule in another class and apply that class to a node. Apply a rule to a node as follows:
Expand Down Expand Up @@ -371,7 +377,7 @@ firewallchain { 'MY_CHAIN:filter:IPv4':
firewall { '100 my rule':
chain => 'MY_CHAIN',
action => 'accept',
jump => 'accept',
proto => 'tcp',
dport => 5000,
}
Expand Down Expand Up @@ -465,7 +471,7 @@ firewall_multi { '100 allow http and https access':
],
dport => [80, 443],
proto => 'tcp',
action => 'accept',
jump => 'accept',
}
```

Expand Down Expand Up @@ -543,4 +549,60 @@ And run the tests from the root of the source code:
bundle exec rake parallel_spec
```

See also `.travis.yml` for information on running the acceptance and other tests.
See the Github Action runs for information on running the acceptance and other tests.

### Migration path to v7.0.0

As of `v7.0.0` of this module a major rework has been done to adopt the [puppet-resource_api](https://github.com/puppetlabs/puppet-resource_api) into the module and use it style of code in place of the original form of Puppet Type and Providers. This was done in the most part to increase the ease with with the module could be maintained and updated in the future, the changes helping to structure the module in such a way as to be more easily understood and altered going forward.

As part of this process several breaking changes where made to the code that will need to be accounted for whenever you update to this new version of the module, with these changes including:

* The `provider` attibute within the `firewall` type has been renamed to `protocol`, both to bring it in line with the matching attribute within the `firewallchain` type and due to the resource_api forbidding the use of `provider` as a attribute name. As part of this the attribute has also been updated to accept `IPv4` and `IPv6` in place of `iptables` or `ip6tables`, though they are still valid as input.
* The `action` attribute within the `firewall` type has been removed as it was merely a restricted version of the `jump` attribute, both of them managing the same function, this being reasoned as a way to enforce the use of generic parameters. From this point the parameters formerly unique to `action` should now be passed to `jump`.
* Strict types have now been implemented for all attributes, while this should not require changes on the user end in most cases, there may be some instances where manifests will require updated to match the new expected form of input.
* Attributes that allow both arrays and negated values have now been updated.
* For attributes that require that all passed values be negated as one, you now merely have to negate the first value within the array, rather than all of them, though negating all is still accepted.
* For attributes that allow passed values to be negated seperately this is not the case. All attributes in this situation are noted within their description.
* The `sport` and `dport` attributes have been updated so that they will now accept with `:` or `-` as a separator when passing ranges, with `:` being preferred as it matchs what is passed to iptables.

Two pairs of manifest taken from the tests can be seen below, illustrating the changes that may be required, the first applying a hoplimit on `ip6tables`:

```Puppet
firewall { '571 - hop_limit':
ensure => present,
proto => 'tcp',
dport => '571',
action => 'ACCEPT',
hop_limit => '5',
provider => 'ip6tables',
}
```

```Puppet
firewall { '571 - hop_limit':
ensure => present,
proto => 'tcp',
dport => '571',
jump => 'accept',
hop_limit => '5',
protocol => 'IPv6',
}
```

And the second negating access to a range of ports on `iptables`:

```puppet
firewall { '560 - negated ports':
proto => `tcp`,
sport => ['! 560-570','! 580'],
action => `accept`,
}
```

```puppet
firewall { '560 - negated ports':
proto => `tcp`,
sport => '! 560:570','580',
jump => `accept`,
}
```
1,583 changes: 936 additions & 647 deletions REFERENCE.md

Large diffs are not rendered by default.

39 changes: 0 additions & 39 deletions lib/puppet/provider/firewall.rb

This file was deleted.

1,088 changes: 1,088 additions & 0 deletions lib/puppet/provider/firewall/firewall.rb

Large diffs are not rendered by default.

331 changes: 0 additions & 331 deletions lib/puppet/provider/firewall/ip6tables.rb

This file was deleted.

1,004 changes: 0 additions & 1,004 deletions lib/puppet/provider/firewall/iptables.rb

This file was deleted.

227 changes: 227 additions & 0 deletions lib/puppet/provider/firewallchain/firewallchain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
# frozen_string_literal: true

require_relative '../../../puppet_x/puppetlabs/firewall/utility'

# Implementation for the firewallchain type using the Resource API.
class Puppet::Provider::Firewallchain::Firewallchain
###### GLOBAL VARIABLES ######

# Command to list all chains and rules
$list_command = {
'IPv4' => 'iptables-save',
'IPv6' => 'ip6tables-save'
}
# Regex used to divide output of$list_command between tables
$table_regex = %r{(\*(?:nat|mangle|filter|raw|rawpost|broute|security)[^*]+)}
# Regex used to retrieve table name
$table_name_regex = %r{^\*(nat|mangle|filter|raw|rawpost|broute|security)}
# Regex used to retrieve Chains
$chain_regex = %r{\n:(INPUT|FORWARD|OUTPUT|(?:\S+))(?:\s(ACCEPT|DROP|QEUE|RETURN|PREROUTING|POSTROUTING))?}
# Base commands for the protocols, including table affixes
$base_command = {
'IPv4' => 'iptables -t',
'IPv6' => 'ip6tables -t'
}
# Command to create a chain
$chain_create_command = '-N'
# Command to flush all rules from a chain, must be used before deleting
$chain_flush_command = '-F'
# Command to delete a chain, cannot be used on inbuilt
$chain_delete_command = '-X'
# Command to set chain policy, works on inbuilt chains only
$chain_policy_command = '-P'
# Check if the given chain name references a built in one
$built_in_regex = %r{^(?:INPUT|OUTPUT|FORWARD|PREROUTING|POSTROUTING)$}

###### PUBLIC METHODS ######

# Raw data is retrieved via `iptables-save` and then regexed to retrieve the different Chains and whether they have a set Policy
def get(_context)
# Create empty return array
chains = []
# Scan String to retrieve all Chains and Policies
['IPv4', 'IPv6'].each do |protocol|
# Retrieve String containing all IPv4 information
iptables_list = Puppet::Provider.execute($list_command[protocol])
iptables_list.scan($table_regex).each do |table|
table_name = table[0].scan($table_name_regex)[0][0]
table[0].scan($chain_regex).each do |chain|
# Create the base hash
chain_hash = {
name: "#{chain[0]}:#{table_name}:#{protocol}",
purge: false,
ignore_foreign: false,
ensure: 'present'
}
# If a policy was found add to the hash
chain_hash[:policy] = chain[1].downcase if chain[1]
chains << chain_hash
end
end
end
# Return array
chains
end

def set(context, changes)
changes.each do |name, change|
is = change[:is]
should = change[:should]

is = PuppetX::Firewall::Utility.create_absent(:name, name) if is.nil?
should = PuppetX::Firewall::Utility.create_absent(:name, name) if should.nil?

# Process the input and divide the name into it's relevant parts
is, should = Puppet::Provider::Firewallchain::Firewallchain.process_input(is, should)
# Run static verification against both sets of values
Puppet::Provider::Firewallchain::Firewallchain.verify(is, should)

if is[:ensure].to_s == 'absent' && should[:ensure].to_s == 'present'
context.creating(name) do
create(context, name, should)
end
elsif is[:ensure].to_s == 'present' && should[:ensure].to_s == 'absent'
context.deleting(name) do
delete(context, name, is)
end
elsif is[:ensure].to_s == 'present' && should[:ensure].to_s == 'present'
context.updating(name) do
update(context, name, should, is)
end
end
end
end

def create(context, name, should)
context.notice("Creating Chain '#{name}' with #{should.inspect}")
Puppet::Provider.execute([$base_command[should[:protocol]], should[:table], $chain_create_command, should[:chain]].join(' '))
PuppetX::Firewall::Utility.persist_iptables(context, name, should[:protocol])
end

def update(context, name, should, is)
# Skip the update if not a inbuilt chain or if policy has not been updated
return if !$built_in_regex.match(should[:chain]) ||
($built_in_regex.match(should[:chain]) && is[:policy] == should[:policy])

context.notice("Updating Chain '#{name}' with #{should.inspect}")
Puppet::Provider.execute([$base_command[should[:protocol]], should[:table], $chain_policy_command, should[:chain], should[:policy].upcase].join(' '))
PuppetX::Firewall::Utility.persist_iptables(context, name, should[:protocol])
end

def delete(context, name, is)
# Before we can delete a chain we must first flush it of any active rules
context.notice("Flushing Chain '#{name}'")
Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], $chain_flush_command, is[:chain]].join(' '))

# For Inbuilt chains we cannot delete them and so instead simply ensure they are reverted to the default policy
if $built_in_regex.match(is[:chain])
context.notice("Reverting Internal Chain '#{name}' to its default")
Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], $chain_policy_command, is[:chain], 'ACCEPT'].join(' '))
else
context.notice("Deleting Chain '#{name}'")
Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], $chain_delete_command, is[:chain]].join(' '))
end
PuppetX::Firewall::Utility.persist_iptables(context, name, is[:protocol])
end

# Custom insync method
def insync?(context, _name, property_name, _is_hash, _should_hash)
context.debug("Checking whether '#{property_name}' is out of sync")

case property_name
when :purge, :ignore, :ignore_foreign
# Suppres any update notifications for the purge/ignore(_foreign) values
# They are used in the generate method which is ran prior to this point and have no
# bearing on it's actual state.
true
else
nil
end
end

###### PRIVATE METHODS ######

# Process the information so that it can be correctly applied
# @api private
def self.process_input(is, should)
# Split the name into it's relevant parts
is[:name] = is[:title] if is[:name].nil?
is[:chain], is[:table], is[:protocol] = is[:name].split(':')
should[:name] = should[:title] if should[:name].nil?
should[:chain], should[:table], should[:protocol] = should[:name].split(':')

# If an in-built chain, always treat it as being present and ensure it is assigned a policy
# The retrieval of in-built chains may get confused by `iptables-save` tendency to not return table information
# for tables that have not yet been interacted with.
is[:ensure] = 'present' if $built_in_regex.match(is[:chain])
is[:policy] = 'accept' if $built_in_regex.match(is[:chain]) && is[:policy].nil?
# For the same reason assign it the default policy as an intended state if it does not have one
should[:policy] = 'accept' if $built_in_regex.match(should[:chain]) && should[:policy].nil?

[is, should]
end

# Verify that the information is correct
# @api private
def self.verify(_is, should)
# Verify that no incorrect chain names are passed
case should[:table]
when 'filter'
raise ArgumentError, 'INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table \'filter\'' if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(should[:chain])
when 'mangle'
raise ArgumentError, 'PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table \'mangle\'' if %r{^(BROUTING)$}.match?(should[:chain])
when 'nat'
raise ArgumentError, 'PREROUTING, POSTROUTING, INPUT, and OUTPUT are the only inbuilt chains that can be used in table \'nat\'' if %r{^(BROUTING|FORWARD)$}.match?(should[:chain])
raise ArgumentError, 'table nat isn\'t valid in IPv6. You must specify \':IPv4\' as the name suffix' if %r{^(IP(v6)?)?$}.match?(should[:protocol])
when 'raw'
raise ArgumentError, 'PREROUTING and OUTPUT are the only inbuilt chains in the table \'raw\'' if %r{^(POSTROUTING|BROUTING|INPUT|FORWARD)$}.match?(should[:chain])
when 'broute'
raise ArgumentError, 'BROUTE is only valid with protocol \'ethernet\'' if should[:protocol] != 'ethernet'
raise ArgumentError, 'BROUTING is the only inbuilt chain allowed on on table \'broute\'' if %r{^PREROUTING|POSTROUTING|INPUT|FORWARD|OUTPUT$}.match?(should[:chain])
when 'security'
raise ArgumentError, 'INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table \'security\'' if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(should[:chain])
end

# Verify that Policy is only passed for the inbuilt chains
raise ArgumentError, "'policy' can only be set on Internal Chains. Setting for '#{should[:name]}' is invalid" if !$built_in_regex.match(should[:chain]) && should.key?(:policy)

# Warn that inbuilt chains will be flushed, not deleted
warn "Warning: Inbuilt Chains may not be deleted. Chain `#{should[:name]}` will be flushed and have it's policy reverted to default." if $built_in_regex.match(should[:chain]) &&
should[:ensure] == 'absent'
end

# Customer generate method called by the resource_api
# Finds and returns all unmanaged rules on the chain that are not set to be ignored
def generate(_context, title, _is, should)
# Unless purge is true, return an empty array
return [] unless should[:purge]

# gather a list of all rules present on the system
rules_resources = Puppet::Type.type(:firewall).instances

# Retrieve information from the title
name, table, protocol = title.split(':')

# Keep only rules in this chain
rules_resources.delete_if do |resource|
resource.rsapi_current_state[:chain] != name || resource.rsapi_current_state[:table] != table || resource.rsapi_current_state[:protocol] != protocol
end

# Remove rules which match our ignore filter
# Ensure ignore value is wrapped as an array to simplify the code
should[:ignore] = [should[:ignore]] if should[:ignore].is_a?(String)
rules_resources.delete_if { |resource| should[:ignore].find_index { |ignore| resource.rsapi_current_state[:line].match(ignore) } } if should[:ignore]

# Remove rules that were (presumably) not put in by puppet
rules_resources.delete_if { |resource| resource.rsapi_current_state[:name].match(%r{^(\d+)[[:graph:][:space:]]})[1].to_i >= 9000 } if should[:ignore_foreign]

# We mark all remaining rules for deletion, and then let the catalog override us on rules which should be present
# We also ensure that the generate rules have the correct protocol to avoid issues with our validation
rules_resources.each do |resource|
resource[:ensure] = :absent
resource[:protocol] = protocol
end

rules_resources
end
end
180 changes: 0 additions & 180 deletions lib/puppet/provider/firewallchain/iptables_chain.rb

This file was deleted.

3,324 changes: 1,159 additions & 2,165 deletions lib/puppet/type/firewall.rb

Large diffs are not rendered by default.

293 changes: 61 additions & 232 deletions lib/puppet/type/firewallchain.rb
Original file line number Diff line number Diff line change
@@ -1,138 +1,63 @@
# frozen_string_literal: true

# This is a workaround for bug: #4248 whereby ruby files outside of the normal
# provider/type path do not load until pluginsync has occured on the puppet server
#
# In this case I'm trying the relative path first, then falling back to normal
# mechanisms. This should be fixed in future versions of puppet but it looks
# like we'll need to maintain this for some time perhaps.
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', '..'))
require 'puppet/util/firewall'

Puppet::Type.newtype(:firewallchain) do
include Puppet::Util::Firewall

@doc = <<-PUPPETCODE
@summary
This type provides the capability to manage rule chains for firewalls.
Currently this supports only iptables, ip6tables and ebtables on Linux. And
provides support for setting the default policy on chains and tables that
allow it.
**Autorequires:**
If Puppet is managing the iptables, iptables-persistent, or iptables-services packages,
and the provider is iptables_chain, the firewall resource will autorequire
those packages to ensure that any required binaries are installed.
#### Providers
* iptables_chain is the only provider that supports firewallchain.
#### Features
* iptables_chain: The provider provides iptables chain features.
* policy: Default policy (inbuilt chains only).
PUPPETCODE

feature :iptables_chain, 'The provider provides iptables chain features.'
feature :policy, 'Default policy (inbuilt chains only)'

ensurable do
defaultvalues
defaultto :present
end

newparam(:name) do
desc <<-PUPPETCODE
The canonical name of the chain.
For iptables the format must be {chain}:{table}:{protocol}.
PUPPETCODE
isnamevar

validate do |value|
if value !~ NAME_FORMAT
raise ArgumentError, 'Inbuilt chains must be in the form {chain}:{table}:{protocol} where {table} is one of filter,' \
' nat, mangle, raw, rawpost, broute, security or empty (alias for filter), chain can be anything without colons' \
' or one of PREROUTING, POSTROUTING, BROUTING, INPUT, FORWARD, OUTPUT for the inbuilt chains, and {protocol} being' \
" IPv4, IPv6, ethernet (ethernet bridging) got '#{value}' table:'#{Regexp.last_match(1)}' chain:'#{Regexp.last_match(2)}' protocol:'#{Regexp.last_match(3)}'"
else
chain = Regexp.last_match(1)
table = Regexp.last_match(2)
protocol = Regexp.last_match(3)
case table
when 'filter'
if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(chain)
raise ArgumentError, "INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table 'filter'"
end
when 'mangle'
if chain =~ INTERNAL_CHAINS && chain == 'BROUTING'
raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table 'mangle'"
end
when 'nat'
if %r{^(BROUTING|FORWARD)$}.match?(chain)
raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, and OUTPUT are the only inbuilt chains that can be used in table 'nat'"
end
if Gem::Version.new(Facter['kernelmajversion'].value.dup) < Gem::Version.new('3.7') && protocol =~ %r{^(IP(v6)?)?$}
raise ArgumentError, "table nat isn't valid in IPv6. You must specify ':IPv4' as the name suffix"
end
when 'raw'
if %r{^(POSTROUTING|BROUTING|INPUT|FORWARD)$}.match?(chain)
raise ArgumentError, 'PREROUTING and OUTPUT are the only inbuilt chains in the table \'raw\''
end
when 'broute'
if protocol != 'ethernet'
raise ArgumentError, 'BROUTE is only valid with protocol \'ethernet\''
end
if %r{^PREROUTING|POSTROUTING|INPUT|FORWARD|OUTPUT$}.match?(chain)
raise ArgumentError, 'BROUTING is the only inbuilt chain allowed on on table \'broute\''
end
when 'security'
if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(chain)
raise ArgumentError, "INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table 'security'"
end
end
if chain == 'BROUTING' && (protocol != 'ethernet' || table != 'broute')
raise ArgumentError, 'BROUTING is the only inbuilt chain allowed on on table \'BROUTE\' with protocol \'ethernet\' i.e. \'broute:BROUTING:enternet\''
end
end
end
end

newproperty(:policy) do
desc <<-PUPPETCODE
This is the action to when the end of the chain is reached.
It can only be set on inbuilt chains (INPUT, FORWARD, OUTPUT,
# lib/puppet/type/firewallchain.rb
require 'puppet/resource_api'

Puppet::ResourceApi.register_type(
name: 'firewallchain',
features: ['custom_generate', 'custom_insync'],
docs: <<-DESC,
This type provides the capability to manage rule chains for firewalls.
Currently this supports only iptables, ip6tables and ebtables on Linux. And
provides support for setting the default policy on chains and tables that
allow it.
#### Providers
* iptables_chain is the only provider that supports firewallchain.
#### Features
* iptables_chain: The provider provides iptables chain features.
* policy: Default policy (inbuilt chains only).
DESC
attributes: {
ensure: {
type: 'Enum[present, absent]',
default: 'present',
desc: <<-DESC
Whether this chain should be present or absent on the target system.
Setting this to absent will first remove all rules associated with this chain and then delete the chain itself.
Inbuilt chains however will merely remove any added rules and, if it has been changed, return their policy to the default.
DESC
},
name: {
type: 'Pattern[/^(?:\S+):(?:nat|mangle|filter|raw|rawpost|broute|security):(?:IP(?:v[46])?|ethernet)$/]',
desc: 'The canonical name of the chain with the required format being `{chain}:{table}:{protocol}`.',
behaviour: :namevar
},
policy: {
type: "Optional[Enum['accept', 'drop', 'queue', 'return']]",
desc: <<-DESC
This action to take when the end of the chain is reached.
This can only be set on inbuilt chains (i.e. INPUT, FORWARD, OUTPUT,
PREROUTING, POSTROUTING) and can be one of:
* accept - the packet is accepted
* drop - the packet is dropped
* queue - the packet is passed userspace
* return - the packet is returned to calling (jump) queue
or the default of inbuilt chains
PUPPETCODE
newvalues(:accept, :drop, :queue, :return)
defaultto do
# ethernet chain have an ACCEPT default while other haven't got an
# allowed value
if @resource[:name] =~ %r{:ethernet$}
:accept
else
nil
end
end
end

newparam(:purge, boolean: true) do
desc <<-PUPPETCODE
Purge unmanaged firewall rules in this chain
PUPPETCODE
newvalues(false, true)
defaultto false
end

newparam(:ignore) do
desc <<-PUPPETCODE
Regex to perform on firewall rules to exempt unmanaged rules from purging (when enabled).
DESC
},
purge: {
type: 'Boolean',
default: false,
desc: 'Whether or not to purge unmanaged rules in this chain'
},
ignore: {
type: 'Optional[Variant[String[1], Array[String[1]]]]',
desc: <<-DESC
Regex to perform on firewall rules to exempt unmanaged rules from purging.
This is matched against the output of `iptables-save`.
This can be a single regex, or an array of them.
Expand All @@ -152,111 +77,15 @@
],
}
```
PUPPETCODE

validate do |value|
unless value.is_a?(Array) || value.is_a?(String) || value == false
devfail 'Ignore must be a string or an Array'
end
end
munge do |patterns| # convert into an array of {Regex}es
patterns = [patterns] if patterns.is_a?(String)
patterns.map { |p| Regexp.new(p) }
end
end

newparam(:ignore_foreign, boolean: true) do
desc <<-PUPPETCODE
DESC
},
ignore_foreign: {
type: 'Boolean',
default: false,
desc: <<-DESC
Ignore rules that do not match the puppet title pattern "^\d+[[:graph:][:space:]]" when purging unmanaged firewall rules in this chain.
This can be used to ignore rules that were not put in by puppet. Beware that nothing keeps other systems from configuring firewall rules with a comment that starts with digits, and is indistinguishable from puppet-configured rules.
PUPPETCODE
newvalues(false, true)
defaultto false
end

# Classes would be a better abstraction, pending:
# http://projects.puppetlabs.com/issues/19001
autorequire(:package) do
case value(:provider)
when :iptables_chain
['iptables', 'iptables-persistent', 'iptables-services']
else
[]
end
end

autorequire(:service) do
case value(:provider)
when :iptables, :ip6tables
['firewalld', 'iptables', 'ip6tables', 'iptables-persistent', 'netfilter-persistent']
else
[]
end
end

validate do
debug('[validate]')

value(:name).match(NAME_FORMAT)
chain = Regexp.last_match(1)
table = Regexp.last_match(2)
protocol = Regexp.last_match(3)

# Check that we're not removing an internal chain
if chain =~ INTERNAL_CHAINS && value(:ensure) == :absent
raise 'Cannot remove in-built chains'
end

if value(:policy).nil? && protocol == 'ethernet'
raise 'you must set a non-empty policy on all ethernet table chains'
end

# Check that we're not setting a policy on a user chain
if chain !~ INTERNAL_CHAINS &&
!value(:policy).nil? &&
protocol != 'ethernet'

raise "policy can only be set on in-built chains (with the exception of ethernet chains) (table:#{table} chain:#{chain} protocol:#{protocol})"
end

# no DROP policy on nat table
if table == 'nat' &&
value(:policy) == :drop

raise 'The "nat" table is not intended for filtering, the use of DROP is therefore inhibited'
end
end

def generate
return [] unless purge?

value(:name).match(NAME_FORMAT)
chain = Regexp.last_match(1)
table = Regexp.last_match(2)
protocol = Regexp.last_match(3)

provider = case protocol
when 'IPv4'
:iptables
when 'IPv6'
:ip6tables
end

# gather a list of all rules present on the system
rules_resources = Puppet::Type.type(:firewall).instances

# Keep only rules in this chain
rules_resources.delete_if { |res| (res[:provider] != provider || res.provider.properties[:table].to_s != table || res.provider.properties[:chain] != chain) }

# Remove rules which match our ignore filter
rules_resources.delete_if { |res| value(:ignore).find_index { |f| res.provider.properties[:line].match(f) } } if value(:ignore)

# Remove rules that were (presumably) not put in by puppet
rules_resources.delete_if { |res| res.provider.properties[:name].match(%r{^(\d+)[[:graph:][:space:]]})[1].to_i >= 9000 } if value(:ignore_foreign) == :true

# We mark all remaining rules for deletion, and then let the catalog override us on rules which should be present
rules_resources.each { |res| res[:ensure] = :absent }

rules_resources
end
end
DESC
}
},
)
262 changes: 0 additions & 262 deletions lib/puppet/util/firewall.rb

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# frozen_string_literal: true

require 'puppet_x'
require 'ipaddr'

module Puppet::Util
module PuppetX::Firewall # rubocop:disable Style/ClassAndModuleChildren
# IPCidr object wrapper for IPAddr
class IPCidr < IPAddr
def initialize(ipaddr, family = Socket::AF_UNSPEC)
super(ipaddr, family)
rescue ArgumentError => e
raise ArgumentError, "Invalid address from IPAddr.new: #{ipaddr}" if %r{invalid address}.match?(e.message)
raise ArgumentError, "Invalid address from IPAddr.new: #{ipaddr}" if e.message.include?('invalid address')

raise e
end

Expand All @@ -26,6 +28,7 @@ def prefixlen
raise 'unsupported address family'
end
return Regexp.last_match(1).length if %r{\A(1*)(0*)\z} =~ (@mask_addr & m).to_s(2)

raise 'bad addr_mask format'
end

Expand Down
293 changes: 293 additions & 0 deletions lib/puppet_x/puppetlabs/firewall/utility.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
# frozen_string_literal: true

require 'puppet_x'
require 'socket'
require 'resolv'
require 'puppet_x/puppetlabs/firewall/ipcidr'

module PuppetX::Firewall # rubocop:disable Style/ClassAndModuleChildren
# A utility class meant to contain re-usable code
class Utility
# Save any current iptables changes so they are retained upon restart
def self.persist_iptables(context, name, protocol)
os_key = Facter.value('os')['family']
cmd = case os_key
when 'RedHat'
case protocol
when 'IPv4', 'iptables'
['/usr/libexec/iptables/iptables.init', 'save']
when 'IPv6', 'ip6tables'
['/usr/libexec/iptables/ip6tables.init', 'save']
end
when 'Debian'
fact = Facter.fact(:iptables_persistent_version)
fact.flush if fact.respond_to?(:flush)
persist_ver = fact.value

case protocol
when 'IPv4', 'IPv6', 'iptables', 'ip6tables'
if persist_ver && Puppet::Util::Package.versioncmp(persist_ver, '1.0').positive?
['/usr/sbin/service', 'netfilter-persistent', 'save']
else
['/usr/sbin/service', 'iptables-persistent', 'save']
end
end
when 'Archlinux'
case protocol
when 'IPv4', 'iptables'
['/bin/sh', '-c', '/usr/sbin/iptables-save > /etc/iptables/iptables.rules']
when 'IPv6', 'ip6tables'
['/bin/sh', '-c', '/usr/sbin/ip6tables-save > /etc/iptables/ip6tables.rules']
end
when 'Suse'
case protocol
when 'IPv4', 'iptables'
['/bin/sh', '-c', '/usr/sbin/iptables-save > /etc/sysconfig/iptables']
end
else
# Catch unsupported OSs
debug('firewall: Rule persistence is not supported for this type/OS')
return
end

# Run the persist command within a rescue block
begin
context.notice("Ensuring changes to '#{name}' persist")
Puppet::Provider.execute(cmd)
rescue Puppet::ExecutionFailure => e
warn "Unable to persist firewall rules: #{e}"
end
end

# @api private
def self.create_absent(namevar, title)
result = if title.is_a? Hash
title.dup
else
{ namevar => title }
end
result[:ensure] = 'absent'
result
end

# Takes an address and protocol and returns the address in CIDR notation.
#
# The protocol is only used when the address is a hostname.
#
# If the address is:
#
# - A hostname:
# It will be resolved
# - An IPv4 address:
# It will be qualified with a /32 CIDR notation
# - An IPv6 address:
# It will be qualified with a /128 CIDR notation
# - An IP address with a CIDR notation:
# It will be normalised
# - An IP address with a dotted-quad netmask:
# It will be converted to CIDR notation
# - Any address with a resulting prefix length of zero:
# It will return nil which is equivilent to not specifying an address
#
def self.host_to_ip(value, proto = nil)
begin
value = PuppetX::Firewall::IPCidr.new(value)
rescue StandardError
family = case proto
when 'IPv4', 'iptables'
Socket::AF_INET
when 'IPv6', 'ip6tables'
Socket::AF_INET6
when nil
raise ArgumentError, 'Proto must be specified for a hostname'
else
raise ArgumentError, "Unsupported address family: #{proto}"
end

new_value = nil
Resolv.each_address(value) do |addr|
begin # rubocop:disable Style/RedundantBegin
new_value = PuppetX::Firewall::IPCidr.new(addr, family)
break
rescue StandardError # looking for the one that works # rubocop:disable Lint/SuppressedException
end
end

raise "Failed to resolve hostname #{value}" if new_value.nil?

value = new_value
end

return nil if value.prefixlen.zero?

value.cidr
end

# Takes an address mask and protocol and converts the host portion to CIDR
# notation.
#
# This takes into account you can negate a mask but follows all rules
# defined in host_to_ip for the host/address part.
#
def self.host_to_mask(value, proto)
match = value.match %r{(!)\s?(.*)$}
return PuppetX::Firewall::Utility.host_to_ip(value, proto) unless match

cidr = PuppetX::Firewall::Utility.host_to_ip(match[2], proto)
return nil if cidr.nil?

"#{match[1]} #{cidr}"
end

# Translate the symbolic names for icmp packet types to integers
def self.icmp_name_to_number(value_icmp, protocol)
if value_icmp.to_s.match?(%r{^\d+$})
value_icmp.to_s
elsif ['IPv4', 'iptables'].include?(protocol)
# https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
case value_icmp
when 'echo-reply' then '0'
when 'destination-unreachable' then '3'
when 'source-quench' then '4'
when 'redirect' then '6'
when 'echo-request' then '8'
when 'router-advertisement' then '9'
when 'router-solicitation' then '10'
when 'time-exceeded' then '11'
when 'parameter-problem' then '12'
when 'timestamp-request' then '13'
when 'timestamp-reply' then '14'
when 'address-mask-request' then '17'
when 'address-mask-reply' then '18'
else nil
end
elsif ['IPv6', 'ip6tables'].include?(protocol)
# https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml
case value_icmp
when 'destination-unreachable' then '1'
when 'too-big' then '2'
when 'time-exceeded' then '3'
when 'parameter-problem' then '4'
when 'echo-request' then '128'
when 'echo-reply' then '129'
when 'router-solicitation' then '133'
when 'router-advertisement' then '134'
when 'neighbour-solicitation' then '135'
when 'neighbour-advertisement' then '136'
when 'redirect' then '137'
else nil
end
else
raise ArgumentError, "unsupported protocol family '#{protocol}'"
end
end

# Convert log_level names to their respective numbers
# https://www.iana.org/assignments/syslog-parameters/syslog-parameters.xhtml
def self.log_level_name_to_number(value)
if value.to_s.match?(%r{^[0-7]$})
value.to_s
else
case value
when 'panic' then '0'
when 'alert' then '1'
when 'crit' then '2'
when 'err', 'error' then '3'
when 'warn', 'warning' then '4'
when 'not', 'notice' then '5'
when 'info' then '6'
when 'debug' then '7'
else nil
end
end
end

# Validates the argument is int or hex, and returns valid hex
# conversion of the value or nil otherwise.
def self.to_hex32(value)
begin
value = Integer(value)
return "0x#{value.to_s(16)}" if value.between?(0, 0xffffffff)
rescue ArgumentError
# pass
end
nil
end

# Accepts a valid mark or mark/mask and returns them in the valid
# hexidecimal format.
# USed for set_mark
def self.mark_mask_to_hex(value)
match = value.to_s.match(%r{([a-fA-F0-9x]+)/?([a-fA-F0-9x]+)?})
mark = PuppetX::Firewall::Utility.to_hex32(match[1])
return "#{mark}/0xffffffff" if match[2].nil?

mask = PuppetX::Firewall::Utility.to_hex32(match[2])
"#{mark}/#{mask}"
end

# Accepts a valid mark and returns them in the valid hexidecimal format.
# Accounts for negation.
# Used for match_mark / connmark
def self.mark_to_hex(value)
match = value.to_s.match(%r{^(!\s)?([a-fA-F0-9x]+)})
mask = PuppetX::Firewall::Utility.to_hex32(match[2])
return mask if match[1].nil?

"! #{mask}"
end

# Converts a given number to its protocol keyword
# https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
def self.proto_number_to_name(value)
return value if %r{^(?:!\s)?([a-z])}.match?(value)

match = value.to_s.match(%r{^(!\s)?(.*)})
keyword = case match[2]
when '1' then 'icmp'
when '2' then 'igmp'
when '4' then 'ipencap'
when '6' then 'tcp'
when '7' then 'cbt'
when '17' then 'udp'
when '47' then 'gre'
when '50' then 'esp'
when '51' then 'ah'
when '89' then 'ospf'
when '103' then 'pim'
when '112' then 'vrrp'
when '132' then 'sctp'
else raise ArgumentError, "Unsupported proto number: #{value}"
end
"#{match[1]}#{keyword}"
end

# Converts a given number to its dscp class name
# https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
def self.dscp_number_to_class(value)
case value
when '0x0a' then 'af11'
when '0x0c' then 'af12'
when '0x0e' then 'af13'
when '0x12' then 'af21'
when '0x14' then 'af22'
when '0x16' then 'af23'
when '0x1a' then 'af31'
when '0x1c' then 'af32'
when '0x1e' then 'af33'
when '0x22' then 'af41'
when '0x24' then 'af42'
when '0x26' then 'af43'
when '0x08' then 'cs1'
when '0x10' then 'cs2'
when '0x18' then 'cs3'
when '0x20' then 'cs4'
when '0x28' then 'cs5'
when '0x30' then 'cs6'
when '0x38' then 'cs7'
when '0x2e' then 'ef'
else nil
end
end
end
end
15 changes: 15 additions & 0 deletions manifests/linux/redhat.pp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,21 @@
#lint:ignore:quoted_booleans
'true',true: {
case $facts['os']['name'] {
'RedHat': {
case $facts['os']['release']['full'] {
/^7\..*/: {
$seluser = 'unconfined_u'
$seltype = 'system_conf_t'
}
default : {
$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 }
}
'CentOS': {
case $facts['os']['release']['full'] {
/^6\..*/: {
Expand Down
4 changes: 2 additions & 2 deletions metadata.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "puppetlabs-firewall",
"version": "6.0.0",
"version": "7.0.0",
"author": "puppetlabs",
"summary": "Manages Firewalls such as iptables",
"license": "Apache-2.0",
"source": "https://github.com/puppetlabs/puppetlabs-firewall",
"project_page": "http://github.com/puppetlabs/puppetlabs-firewall",
"issues_url": "https://tickets.puppetlabs.com/browse/MODULES",
"issues_url": "https://github.com/puppetlabs/puppetlabs-firewall/issues",
"dependencies": [
{
"name": "puppetlabs/stdlib",
Expand Down
8 changes: 4 additions & 4 deletions provision.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@ vagrant:
images:
- centos/7
- generic/ubuntu1804
travis_deb:
docker_deb:
provisioner: docker
images:
- litmusimage/debian:8
- litmusimage/debian:9
travis_ub_6:
docker_ub_6:
provisioner: docker
images:
- litmusimage/ubuntu:16.04
- litmusimage/ubuntu:18.04
- litmusimage/ubuntu:20.04
travis_el7:
docker_el7:
provisioner: docker
images:
- litmusimage/centos:7
travis_el8:
docker_el8:
provisioner: docker
images:
- litmusimage/centos:8
Expand Down
10 changes: 4 additions & 6 deletions spec/acceptance/class_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,20 @@

describe 'firewall class' do
before(:all) do
if os[:family] == 'ubuntu' || os[:family] == 'debian'
update_profile_file
end
update_profile_file if os[:family] == 'ubuntu' || os[:family] == 'debian'
end

it 'runs successfully', unless: os[:family] == 'redhat' && os[:release].to_i == 6 do
it 'runs successfully' do
pp = "class { 'firewall': }"
idempotent_apply(pp)
end

it 'ensure => stopped:', unless: os[:family] == 'redhat' && os[:release].to_i == 6 do
it 'ensure => stopped:' do
pp = "class { 'firewall': ensure => stopped }"
idempotent_apply(pp)
end

it 'ensure => running:', unless: os[:family] == 'redhat' && os[:release].to_i == 6 do
it 'ensure => running:' do
pp = "class { 'firewall': ensure => running }"
idempotent_apply(pp)
end
Expand Down
1,027 changes: 462 additions & 565 deletions spec/acceptance/firewall_attributes_exceptions_spec.rb

Large diffs are not rendered by default.

339 changes: 188 additions & 151 deletions spec/acceptance/firewall_attributes_happy_path_spec.rb

Large diffs are not rendered by default.

414 changes: 231 additions & 183 deletions spec/acceptance/firewall_attributes_ipv6_exceptions_spec.rb

Large diffs are not rendered by default.

312 changes: 168 additions & 144 deletions spec/acceptance/firewall_attributes_ipv6_happy_path_spec.rb

Large diffs are not rendered by default.

12 changes: 5 additions & 7 deletions spec/acceptance/firewall_duplicate_comment_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

describe 'firewall - duplicate comments' do
before(:all) do
if os[:family] == 'ubuntu' || os[:family] == 'debian'
update_profile_file
end
update_profile_file if os[:family] == 'ubuntu' || os[:family] == 'debian'
end

after(:each) do
Expand All @@ -21,9 +19,9 @@ class { 'firewall': }
}
firewall { '550 destination':
proto => tcp,
dport => '550',
action => accept,
proto => tcp,
dport => '550',
jump => accept,
destination => '192.168.2.0/24',
}
PUPPETCODE
Expand All @@ -33,7 +31,7 @@ class { 'firewall': }
run_shell('iptables -I INPUT -m state --state NEW -m tcp -p tcp --dport 552 -j ACCEPT -m comment --comment "550 destination"')

apply_manifest(pp) do |r|
expect(r.stderr).to include('Duplicate rule found for 550 destination. Skipping update.')
expect(r.stderr).to include('Duplicate names have been found within your Firewalls. This prevents the module from working correctly and must be manually resolved.')
end
end
end
Expand Down
60 changes: 49 additions & 11 deletions spec/acceptance/firewallchain_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
ip6tables_flush_all_tables
end

describe 'ensure' do
describe 'IPv4' do
context 'when present' do
pp1 = <<-PUPPETCODE
firewallchain { 'MY_CHAIN:filter:IPv4':
Expand Down Expand Up @@ -46,9 +46,47 @@
end
end

describe 'IPv6' do
context 'when present' do
pp3 = <<-PUPPETCODE
firewallchain { 'MY_CHAIN:filter:IPv6':
ensure => present,
}
PUPPETCODE
it 'applies cleanly' do
# Run it twice and test for idempotency
idempotent_apply(pp3)
end

it 'finds the chain' do
run_shell('ip6tables-save') do |r|
expect(r.stdout).to match(%r{MY_CHAIN})
end
end
end

context 'when absent' do
pp4 = <<-PUPPETCODE
firewallchain { 'MY_CHAIN:filter:IPv6':
ensure => absent,
}
PUPPETCODE
it 'applies cleanly' do
# Run it twice and test for idempotency
idempotent_apply(pp4)
end

it 'fails to find the chain' do
run_shell('ip6tables-save') do |r|
expect(r.stdout).not_to match(%r{MY_CHAIN})
end
end
end
end

# XXX purge => false is not yet implemented
# context 'when adding a firewall rule to a chain:' do
# pp3 = <<-PUPPETCODE
# pp5 = <<-PUPPETCODE
# firewallchain { 'MY_CHAIN:filter:IPv4':
# ensure => present,
# }
Expand All @@ -61,13 +99,13 @@
# PUPPETCODE
# it 'applies cleanly' do
# # Run it twice and test for idempotency
# apply_manifest(pp3, :catch_failures => true)
# apply_manifest(pp3, :catch_changes => do_catch_changes)
# apply_manifest(pp5, :catch_failures => true)
# apply_manifest(pp5, :catch_changes => do_catch_changes)
# end
# end

# context 'when not purge firewallchain chains:' do
# pp4 = <<-PUPPETCODE
# pp6 = <<-PUPPETCODE
# firewallchain { 'MY_CHAIN:filter:IPv4':
# ensure => present,
# purge => false,
Expand All @@ -79,14 +117,14 @@
# PUPPETCODE
# it 'does not purge the rule' do
# # Run it twice and test for idempotency
# apply_manifest(pp4, :catch_failures => true) do |r|
# apply_manifest(pp6, :catch_failures => true) do |r|
# expect(r.stdout).to_not match(/removed/)
# expect(r.stderr).to eq('')
# end
# apply_manifest(pp4, :catch_changes => do_catch_changes)
# apply_manifest(pp6, :catch_changes => do_catch_changes)
# end

# pp5 = <<-PUPPETCODE
# pp7 = <<-PUPPETCODE
# firewall { '100 my rule':
# chain => 'MY_CHAIN',
# action => 'accept',
Expand All @@ -96,7 +134,7 @@
# PUPPETCODE
# it 'still has the rule' do
# # Run it twice and test for idempotency
# apply_manifest(pp5, :catch_changes => do_catch_changes)
# apply_manifest(pp7, :catch_changes => do_catch_changes)
# end
# end

Expand All @@ -106,14 +144,14 @@
end

context 'when DROP' do
pp6 = <<-PUPPETCODE
pp8 = <<-PUPPETCODE
firewallchain { 'FORWARD:filter:IPv4':
policy => 'drop',
}
PUPPETCODE
it 'applies cleanly' do
# Run it twice and test for idempotency
idempotent_apply(pp6)
idempotent_apply(pp8)
end

it 'finds the chain' do
Expand Down
12 changes: 8 additions & 4 deletions spec/acceptance/resource_cmd_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
before(:all) do
# In order to properly check stderr for anomalies we need to fix the deprecation warnings from puppet.conf.
config = run_shell('puppet config print config').stdout
run_shell("sed -i -e \'s/^templatedir.*$//\' #{config}")
run_shell("sed -i -e 's/^templatedir.*$//' #{config}")
if fetch_os_name == 'redhat' && [6, 7].include?(os[:release].to_i)
run_shell('echo export LC_ALL="C" > /etc/profile.d/my-custom.lang.sh')
run_shell('echo "## US English ##" >> /etc/profile.d/my-custom.lang.sh')
Expand All @@ -24,6 +24,11 @@
end

context 'when make sure it returns no errors when executed on a clean machine' do
before(:all) do
iptables_flush_all_tables
ip6tables_flush_all_tables
end

run_shell('locale')
let(:result) { run_shell('puppet resource firewall') }

Expand Down Expand Up @@ -145,7 +150,7 @@
end
end

context 'when accepts rules with negation' do
context 'when accepts rules with --dir' do
before :all do
iptables_flush_all_tables
run_shell('iptables -t nat -A POSTROUTING -s 192.168.122.0/24 -m policy --dir out --pol ipsec -j ACCEPT')
Expand Down Expand Up @@ -197,10 +202,9 @@
end
end

# version of iptables that ships with el5 doesn't work with the
# ip6tables provider
# TODO: Test below fails if this file is run seperately. i.e. bundle exec rspec spec/acceptance/resource_cmd_spec.rb
context 'when dport/sport with ip6tables', unless: os[:family] == 'redhat' && os[:release].start_with?('5') do
context 'when dport/sport with ip6tables' do
before :all do
if os['family'] == 'debian'
run_shell('echo "iptables-persistent iptables-persistent/autosave_v4 boolean false" | debconf-set-selections')
Expand Down
95 changes: 47 additions & 48 deletions spec/acceptance/rules_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@
describe 'rules spec' do
describe 'complex ruleset 1' do
before :all do
if os[:family] == 'redhat'
pre_setup
end
pre_setup if os[:family] == 'redhat'
iptables_flush_all_tables
ip6tables_flush_all_tables
end
Expand All @@ -26,31 +24,31 @@
proto => 'all',
source => '10.0.0.0/8',
destination => '10.0.0.0/8',
action => 'accept',
jump => 'ACCEPT',
}
firewall { '100 forward standard allow tcp':
chain => 'FORWARD',
source => '10.0.0.0/8',
destination => '!10.0.0.0/8',
destination => '! 10.0.0.0/8',
proto => 'tcp',
ctstate => 'NEW',
sport => [80,443,21,20,22,53,123,43,873,25,465],
action => 'accept',
ctstate => 'NEW',
sport => ['80','443','21','20','22','53','123','43','873','25','465'],
jump => 'ACCEPT',
}
firewall { '100 forward standard allow udp':
chain => 'FORWARD',
source => '10.0.0.0/8',
destination => '!10.0.0.0/8',
destination => '! 10.0.0.0/8',
proto => 'udp',
sport => [53,123],
action => 'accept',
sport => ['53','123'],
jump => 'ACCEPT',
}
firewall { '100 forward standard allow icmp':
chain => 'FORWARD',
source => '10.0.0.0/8',
destination => '!10.0.0.0/8',
destination => '! 10.0.0.0/8',
proto => 'icmp',
action => 'accept',
jump => 'ACCEPT',
}
firewall { '090 ignore ipsec':
Expand All @@ -59,28 +57,28 @@
outiface => 'eth0',
ipsec_policy => 'ipsec',
ipsec_dir => 'out',
action => 'accept',
jump => 'ACCEPT',
}
firewall { '093 ignore 10.0.0.0/8':
table => 'nat',
chain => 'POSTROUTING',
outiface => 'eth0',
destination => '10.0.0.0/8',
action => 'accept',
jump => 'ACCEPT',
}
firewall { '093 ignore 172.16.0.0/12':
table => 'nat',
chain => 'POSTROUTING',
outiface => 'eth0',
destination => '172.16.0.0/12',
action => 'accept',
jump => 'ACCEPT',
}
firewall { '093 ignore 192.168.0.0/16':
table => 'nat',
chain => 'POSTROUTING',
outiface => 'eth0',
destination => '192.168.0.0/16',
action => 'accept',
jump => 'ACCEPT',
}
firewall { '100 masq outbound':
table => 'nat',
Expand All @@ -101,12 +99,13 @@
it 'applies cleanly' do
idempotent_apply(pp1)
end

regex_values = [
%r{INPUT ACCEPT}, %r{FORWARD ACCEPT}, %r{OUTPUT ACCEPT},
%r{-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) -d 10.0.0.0\/(8|255\.0\.0\.0) -m comment --comment \"090 forward allow local\" -j ACCEPT},
%r{-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0\/(8|255\.0\.0\.0) -p icmp -m comment --comment \"100 forward standard allow icmp\" -j ACCEPT},
%r{-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0\/(8|255\.0\.0\.0) -p tcp -m multiport --sports 80,443,21,20,22,53,123,43,873,25,465 -m conntrack --ctstate NEW -m comment --comment \"100 forward standard allow tcp\" -j ACCEPT}, # rubocop:disable Layout/LineLength
%r{-A FORWARD -s 10.0.0.0\/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0\/(8|255\.0\.0\.0) -p udp -m multiport --sports 53,123 -m comment --comment \"100 forward standard allow udp\" -j ACCEPT}
%r{-A FORWARD -s 10.0.0.0/(8|255\.0\.0\.0) -d 10.0.0.0/(8|255\.0\.0\.0) -m comment --comment "090 forward allow local" -j ACCEPT},
%r{-A FORWARD -s 10.0.0.0/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0/(8|255\.0\.0\.0) -p (icmp|1) -m comment --comment "100 forward standard allow icmp" -j ACCEPT},
%r{-A FORWARD -s 10.0.0.0/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0/(8|255\.0\.0\.0) -p (tcp|6) -m multiport --sports 80,443,21,20,22,53,123,43,873,25,465 -m conntrack --ctstate NEW -m comment --comment "100 forward standard allow tcp" -j ACCEPT}, # rubocop:disable Layout/LineLength
%r{-A FORWARD -s 10.0.0.0/(8|255\.0\.0\.0) (! -d|-d !) 10.0.0.0/(8|255\.0\.0\.0) -p (udp|17) -m multiport --sports 53,123 -m comment --comment "100 forward standard allow udp" -j ACCEPT}
]
it 'contains appropriate rules' do
run_shell('iptables-save') do |r|
Expand All @@ -126,13 +125,13 @@
end

pp2 = <<-PUPPETCODE
class { '::firewall': }
class { 'firewall': }
Firewall {
proto => 'all',
}
Firewallchain {
purge => 'true',
purge => true,
ignore => [
'--comment "[^"]*(?i:ignore)[^"]*"',
],
Expand All @@ -141,33 +140,33 @@ class { '::firewall': }
firewall { '001 ssh needed for beaker testing':
proto => 'tcp',
dport => '22',
action => 'accept',
jump => 'ACCEPT',
before => Firewallchain['INPUT:filter:IPv4'],
}
firewall { '010 INPUT allow established and related':
proto => 'all',
ctstate => ['ESTABLISHED', 'RELATED'],
action => 'accept',
jump => 'ACCEPT',
before => Firewallchain['INPUT:filter:IPv4'],
}
firewall { "011 reject local traffic not on loopback interface":
iniface => '! lo',
proto => 'all',
destination => '127.0.0.1/8',
action => 'reject',
destination => '127.0.0.0/8',
jump => 'REJECT',
}
firewall { '012 accept loopback':
iniface => 'lo',
action => 'accept',
jump => 'ACCEPT',
before => Firewallchain['INPUT:filter:IPv4'],
}
firewall { '020 ssh':
proto => 'tcp',
dport => '22',
ctstate => 'NEW',
action => 'accept',
jump => 'ACCEPT',
before => Firewallchain['INPUT:filter:IPv4'],
}
Expand All @@ -177,29 +176,29 @@ class { '::firewall': }
proto => 'tcp',
dport => '25',
ctstate => 'NEW',
action => 'accept',
jump => 'ACCEPT',
}
firewall { '013 icmp echo-request':
proto => 'icmp',
icmp => 'echo-request',
action => 'accept',
jump => 'ACCEPT',
source => '10.0.0.0/8',
}
firewall { '013 icmp destination-unreachable':
proto => 'icmp',
icmp => 'destination-unreachable',
action => 'accept',
jump => 'ACCEPT',
}
firewall { '013 icmp time-exceeded':
proto => 'icmp',
icmp => 'time-exceeded',
action => 'accept',
jump => 'ACCEPT',
}
firewall { '443 ssl on aliased interface':
proto => 'tcp',
dport => '443',
ctstate => 'NEW',
action => 'accept',
jump => 'ACCEPT',
iniface => 'eth0:3',
}
Expand All @@ -226,7 +225,7 @@ class { '::firewall': }
chain => 'FORWARD',
proto => 'all',
ctstate => ['ESTABLISHED','RELATED'],
action => 'accept',
jump => 'ACCEPT',
before => Firewallchain['FORWARD:filter:IPv4'],
}
firewallchain { 'FORWARD:filter:IPv4':
Expand All @@ -253,19 +252,19 @@ class { '::firewall': }
%r{OUTPUT ACCEPT},
%r{LOCAL_INPUT},
%r{LOCAL_INPUT_PRE},
%r{-A INPUT -m comment --comment \"001 LOCAL_INPUT_PRE\" -j LOCAL_INPUT_PRE},
%r{-A INPUT -p tcp -m multiport --dports 22 -m comment --comment \"001 ssh needed for beaker testing\" -j ACCEPT},
%r{-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment \"010 INPUT allow established and related\" -j ACCEPT},
%r{-A INPUT -d 127.0.0.0\/(8|255\.0\.0\.0) (! -i|-i !) lo -m comment --comment \"011 reject local traffic not on loopback interface\" -j REJECT --reject-with icmp-port-unreachable},
%r{-A INPUT -i lo -m comment --comment \"012 accept loopback\" -j ACCEPT},
%r{-A INPUT -p icmp -m icmp --icmp-type 3 -m comment --comment \"013 icmp destination-unreachable\" -j ACCEPT},
%r{-A INPUT -s 10.0.0.0\/(8|255\.0\.0\.0) -p icmp -m icmp --icmp-type 8 -m comment --comment \"013 icmp echo-request\" -j ACCEPT},
%r{-A INPUT -p icmp -m icmp --icmp-type 11 -m comment --comment \"013 icmp time-exceeded\" -j ACCEPT},
%r{-A INPUT -p tcp -m multiport --dports 22 -m conntrack --ctstate NEW -m comment --comment \"020 ssh\" -j ACCEPT},
%r{-A INPUT -i eth0:3 -p tcp -m multiport --dports 443 -m conntrack --ctstate NEW -m comment --comment \"443 ssl on aliased interface\" -j ACCEPT},
%r{-A INPUT -m comment --comment \"900 LOCAL_INPUT\" -j LOCAL_INPUT},
%r{-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment \"010 allow established and related\" -j ACCEPT},
%r{-A OUTPUT (! -o|-o !) eth0:2 -p tcp -m multiport --dports 25 -m conntrack --ctstate NEW -m comment --comment \"025 smtp\" -j ACCEPT},
%r{-A INPUT -m comment --comment "001 LOCAL_INPUT_PRE" -j LOCAL_INPUT_PRE},
%r{-A INPUT -p (tcp|6) -m tcp --dport 22 -m comment --comment "001 ssh needed for beaker testing" -j ACCEPT},
%r{-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment "010 INPUT allow established and related" -j ACCEPT},
%r{-A INPUT -d 127.0.0.0/(8|255\.0\.0\.0) (! -i|-i !) lo -m comment --comment "011 reject local traffic not on loopback interface" -j REJECT --reject-with icmp-port-unreachable},
%r{-A INPUT -i lo -m comment --comment "012 accept loopback" -j ACCEPT},
%r{-A INPUT -p (icmp|1) -m icmp --icmp-type 3 -m comment --comment "013 icmp destination-unreachable" -j ACCEPT},
%r{-A INPUT -s 10.0.0.0/(8|255\.0\.0\.0) -p (icmp|1) -m icmp --icmp-type 8 -m comment --comment "013 icmp echo-request" -j ACCEPT},
%r{-A INPUT -p (icmp|1) -m icmp --icmp-type 11 -m comment --comment "013 icmp time-exceeded" -j ACCEPT},
%r{-A INPUT -p (tcp|6) -m tcp --dport 22 -m conntrack --ctstate NEW -m comment --comment "020 ssh" -j ACCEPT},
%r{-A INPUT -i eth0:3 -p (tcp|6) -m tcp --dport 443 -m conntrack --ctstate NEW -m comment --comment "443 ssl on aliased interface" -j ACCEPT},
%r{-A INPUT -m comment --comment "900 LOCAL_INPUT" -j LOCAL_INPUT},
%r{-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment "010 allow established and related" -j ACCEPT},
%r{-A OUTPUT (! -o|-o !) eth0:2 -p (tcp|6) -m tcp --dport 25 -m conntrack --ctstate NEW -m comment --comment "025 smtp" -j ACCEPT},
]
it 'contains appropriate rules' do
run_shell('iptables-save') do |r|
Expand Down
16 changes: 8 additions & 8 deletions spec/acceptance/standard_usage_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,28 @@ class my_fw::pre {
# Default firewall rules
firewall { '000 accept all icmp':
proto => 'icmp',
action => 'accept',
jump => 'ACCEPT',
}->
firewall { '001 accept all to lo interface':
proto => 'all',
iniface => 'lo',
action => 'accept',
jump => 'ACCEPT',
}->
firewall { "0002 reject local traffic not on loopback interface":
iniface => '! lo',
destination => '127.0.0.1/8',
action => 'reject',
destination => '127.0.0.0/8',
jump => 'REJECT',
}->
firewall { '003 accept related established rules':
proto => 'all',
ctstate => ['RELATED', 'ESTABLISHED'],
action => 'accept',
jump => 'ACCEPT',
}
}
class my_fw::post {
firewall { '999 drop all':
proto => 'all',
action => 'drop',
jump => 'DROP',
before => undef,
}
}
Expand All @@ -48,9 +48,9 @@ class my_fw::post {
class { ['my_fw::pre', 'my_fw::post']: }
class { 'firewall': }
firewall { '500 open up port 22':
action => 'accept',
jump => 'ACCEPT',
proto => 'tcp',
dport => 22,
dport => '22',
}
PUPPETCODE
it 'applies twice' do
Expand Down
Loading