178 changes: 147 additions & 31 deletions lib/puppet/provider/firewall/iptables.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

has_feature :iptables
has_feature :rate_limiting
has_feature :recent_limiting
has_feature :snat
has_feature :dnat
has_feature :interface_match
Expand All @@ -24,6 +25,8 @@
has_feature :socket
has_feature :address_type
has_feature :iprange
has_feature :ipsec_dir
has_feature :ipsec_policy

optional_commands({
:iptables => 'iptables',
Expand All @@ -43,10 +46,11 @@

@resource_map = {
:burst => "--limit-burst",
:ctstate => "-m conntrack --ctstate",
:destination => "-d",
:dst_type => "-m addrtype --dst-type",
:dst_range => "-m iprange --dst-range",
:dport => ["-m multiport --dports", "-m (udp|tcp) --dport"],
:dport => ["-m multiport --dports", "--dport"],
:gid => "-m owner --gid-owner",
:icmp => "-m icmp --icmp-type",
:iniface => "-i",
Expand All @@ -58,13 +62,22 @@
:outiface => "-o",
:port => '-m multiport --ports',
:proto => "-p",
:random => "--random",
:rdest => "--rdest",
:reap => "--reap",
:recent => "-m recent",
:reject => "--reject-with",
:rhitcount => "--hitcount",
:rname => "--name",
:rseconds => "--seconds",
:rsource => "--rsource",
:rttl => "--rttl",
:set_mark => mark_flag,
:socket => "-m socket",
:source => "-s",
:src_type => "-m addrtype --src-type",
:src_range => "-m iprange --src-range",
:sport => ["-m multiport --sports", "-m (udp|tcp) --sport"],
:sport => ["-m multiport --sports", "--sport"],
:state => "-m state --state",
:table => "-t",
:tcp_flags => "-m tcp --tcp-flags",
Expand All @@ -74,28 +87,62 @@
:uid => "-m owner --uid-owner",
:pkttype => "-m pkttype --pkt-type",
:isfragment => "-f",
:ipsec_dir => "-m policy --dir",
:ipsec_policy => "--pol",
}

# These are known booleans that do not take a value, but we want to munge
# to true if they exist.
@known_booleans = [
:isfragment,
:random,
:rdest,
:reap,
:rsource,
:rttl,
:socket
]


# Create property methods dynamically
(@resource_map.keys << :chain << :table << :action).each do |property|
define_method "#{property}" do
@property_hash[property.to_sym]
if @known_booleans.include?(property) then
# The boolean properties default to '' which should be read as false
define_method "#{property}" do
@property_hash[property] = :false if @property_hash[property] == nil
@property_hash[property.to_sym]
end
else
define_method "#{property}" do
@property_hash[property.to_sym]
end
end

define_method "#{property}=" do |value|
@property_hash[:needs_change] = true
if property == :chain
define_method "#{property}=" do |value|
if @property_hash[:chain] != value
raise ArgumentError, "Modifying the chain for existing rules is not supported."
end
end
else
define_method "#{property}=" do |value|
@property_hash[:needs_change] = true
end
end
end

# This is the order of resources as they appear in iptables-save output,
# we need it to properly parse and apply rules, if the order of resource
# changes between puppet runs, the changed rules will be re-applied again.
# This order can be determined by going through iptables source code or just tweaking and trying manually
@resource_list = [:table, :source, :src_range, :destination, :dst_range, :iniface, :outiface,
:proto, :isfragment, :tcp_flags, :gid, :uid, :sport, :dport, :port,
:dst_type, :src_type, :socket, :pkttype, :name, :state, :icmp,
:limit, :burst, :jump, :todest, :tosource, :toports, :log_prefix,
:log_level, :reject, :set_mark]
@resource_list = [
:table, :source, :destination, :iniface, :outiface, :proto, :isfragment,
:src_range, :dst_range, :tcp_flags, :gid, :uid, :sport, :dport, :port,
:dst_type, :src_type, :socket, :pkttype, :name, :ipsec_dir, :ipsec_policy,
:state, :ctstate, :icmp, :limit, :burst, :recent, :rseconds, :reap,
:rhitcount, :rttl, :rname, :rsource, :rdest, :jump, :todest, :tosource,
:toports, :random, :log_prefix, :log_level, :reject, :set_mark
]

def insert
debug 'Inserting rule %s' % resource[:name]
Expand Down Expand Up @@ -154,36 +201,54 @@ def self.rule_to_hash(line, table, counter)
keys = []
values = line.dup

# These are known booleans that do not take a value, but we want to munge
# to true if they exist.
known_booleans = [:socket, :isfragment]

####################
# PRE-PARSE CLUDGING
####################

# --tcp-flags takes two values; we cheat by adding " around it
# so it behaves like --comment
values = values.sub(/--tcp-flags (\S*) (\S*)/, '--tcp-flags "\1 \2"')
# we do a similar thing for negated address masks (source and destination).
values = values.sub(/(-\S+) (!)\s?(\S*)/,'\1 "\2 \3"')
# the actual rule will have the ! mark before the option.
values = values.sub(/(!)\s*(-\S+)\s*(\S*)/, '\2 "\1 \3"')
# The match extension for tcp & udp are optional and throws off the @resource_map.
values = values.sub(/-m (tcp|udp) (--(s|d)port|-m multiport)/, '\2')

# Trick the system for booleans
known_booleans.each do |bool|
if bool == :socket then
values = values.sub(/#{@resource_map[bool]}/, '-m socket true')
end
@known_booleans.each do |bool|
# append "true" because all params are expected to have values
if bool == :isfragment then
# -f requires special matching:
# only replace those -f that are not followed by an l to
# distinguish between -f and the '-f' inside of --tcp-flags.
values = values.sub(/-f(?!l)(?=.*--comment)/, '-f true')
else
values = values.sub(/#{@resource_map[bool]}/, "#{@resource_map[bool]} true")
end
end

############
# Populate parser_list with used value, in the correct order
############
map_index={}
@resource_map.each_pair do |map_k,map_v|
[map_v].flatten.each do |v|
ind=values.index(/\s#{v}/)
next unless ind
map_index[map_k]=ind
end
end
# Generate parser_list based on the index of the found option
parser_list=[]
map_index.sort_by{|k,v| v}.each{|mapi| parser_list << mapi.first }

############
# MAIN PARSE
############

# Here we iterate across our values to generate an array of keys
@resource_list.reverse.each do |k|
parser_list.reverse.each do |k|
resource_map_key = @resource_map[k]
[resource_map_key].flatten.each do |opt|
if values.slice!(/\s#{opt}/)
Expand All @@ -206,17 +271,20 @@ def self.rule_to_hash(line, table, counter)

# Normalise all rules to CIDR notation.
[:source, :destination].each do |prop|
hash[prop] = Puppet::Util::IPCidr.new(hash[prop]).cidr unless hash[prop].nil?
next if hash[prop].nil?
m = hash[prop].match(/(!?)\s?(.*)/)
neg = "! " if m[1] == "!"
hash[prop] = "#{neg}#{Puppet::Util::IPCidr.new(m[2]).cidr}"
end

[:dport, :sport, :port, :state].each do |prop|
[:dport, :sport, :port, :state, :ctstate].each do |prop|
hash[prop] = hash[prop].split(',') if ! hash[prop].nil?
end

# Convert booleans removing the previous cludge we did
known_booleans.each do |bool|
@known_booleans.each do |bool|
if hash[bool] != nil then
unless hash[bool] == "true" then
if hash[bool] != "true" then
raise "Parser error: #{bool} was meant to be a boolean but received value: #{hash[bool]}."
end
end
Expand All @@ -234,7 +302,8 @@ def self.rule_to_hash(line, table, counter)

# States should always be sorted. This ensures that the output from
# iptables-save and user supplied resources is consistent.
hash[:state] = hash[:state].sort unless hash[:state].nil?
hash[:state] = hash[:state].sort unless hash[:state].nil?
hash[:ctstate] = hash[:ctstate].sort unless hash[:ctstate].nil?

# This forces all existing, commentless rules or rules with invalid comments to be moved
# to the bottom of the stack.
Expand Down Expand Up @@ -309,17 +378,21 @@ def general_args
args = []
resource_list = self.class.instance_variable_get('@resource_list')
resource_map = self.class.instance_variable_get('@resource_map')
known_booleans = self.class.instance_variable_get('@known_booleans')

resource_list.each do |res|
resource_value = nil
if (resource[res]) then
resource_value = resource[res]
# If socket is true then do not add the value as -m socket is standalone
if res == :socket then
resource_value = nil
end
if res == :isfragment then
resource_value = nil
if known_booleans.include?(res) then
if resource[res] == :true then
resource_value = nil
else
# If the property is not :true then we don't want to add the value
# to the args list
next
end
end
elsif res == :jump and resource[:action] then
# In this case, we are substituting jump for action
Expand All @@ -330,6 +403,14 @@ def general_args

args << [resource_map[res]].flatten.first.split(' ')

# On negations, the '!' has to be before the option (eg: "! -d 1.2.3.4")
if resource_value.is_a?(String) and resource_value.sub!(/^!\s*/, '') then
# we do this after adding the 'dash' argument because of ones like "-m multiport --dports", where we want it before the "--dports" but after "-m multiport".
# so we insert before whatever the last argument is
args.insert(-2, '!')
end


# For sport and dport, convert hyphens to colons since the type
# expects hyphens for ranges of ports.
if [:sport, :dport, :port].include?(res) then
Expand Down Expand Up @@ -369,8 +450,43 @@ def insert_order
# No rules at all? Just bail now.
return 1 if rules.empty?

# Add our rule to the end of the array of known rules
my_rule = resource[:name].to_s
rules << my_rule
rules.sort.index(my_rule) + 1

unmanaged_rule_regex = /^9[0-9]{3}\s[a-f0-9]{32}$/
# Find if this is a new rule or an existing rule, then find how many
# unmanaged rules preceed it.
if rules.length == rules.uniq.length
# This is a new rule so find its ordered location.
new_rule_location = rules.sort.uniq.index(my_rule)
if new_rule_location == 0
# The rule will be the first rule in the chain because nothing came
# before it.
offset_rule = rules[0]
else
# This rule will come after other managed rules, so find the rule
# immediately preceeding it.
offset_rule = rules.sort.uniq[new_rule_location - 1]
end
else
# This is a pre-existing rule, so find the offset from the original
# ordering.
offset_rule = my_rule
end
# Count how many unmanaged rules are ahead of the target rule so we know
# how much to add to the insert order
unnamed_offset = rules[0..rules.index(offset_rule)].inject(0) do |sum,rule|
# This regex matches the names given to unmanaged rules (a number
# 9000-9999 followed by an MD5 hash).
sum + (rule.match(unmanaged_rule_regex) ? 1 : 0)
end

# We want our rules to come before unmanaged rules
unnamed_offset -= 1 if offset_rule.match(unmanaged_rule_regex)

# Insert our new or updated rule in the correct order of named rules, but
# offset for unnamed rules.
rules.sort.index(my_rule) + 1 + unnamed_offset
end
end
27 changes: 19 additions & 8 deletions lib/puppet/provider/firewallchain/iptables_chain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@
Nameformat = /^(.+):(#{Tables}):(IP(v[46])?|ethernet)$/

def create
# can't create internal chains
if @resource[:name] =~ InternalChains
self.warn "Attempting to create internal chain #{@resource[:name]}"
end
allvalidchains do |t, chain, table, protocol|
if chain =~ InternalChains
# can't create internal chains
warning "Attempting to create internal chain #{@resource[:name]}"
end
if properties[:ensure] == protocol
debug "Skipping Inserting chain #{chain} on table #{table} (#{protocol}) already exists"
else
Expand All @@ -59,17 +59,28 @@ def create
end

def destroy
# can't delete internal chains
if @resource[:name] =~ InternalChains
self.warn "Attempting to destroy internal chain #{@resource[:name]}"
end
allvalidchains do |t, chain, table|
if chain =~ InternalChains
# can't delete internal chains
warning "Attempting to destroy internal chain #{@resource[:name]}"
end
debug "Deleting chain #{chain} on table #{table}"
t.call ['-t',table,'-X',chain]
end
end

def exists?
allvalidchains do |t, chain|
if chain =~ InternalChains
# If the chain isn't present, it's likely because the module isn't loaded.
# If this is true, then we fall into 2 cases
# 1) It'll be loaded on demand
# 2) It won't be loaded on demand, and we throw an error
# This is the intended behavior as it's not the provider's job to load kernel modules
# So we pretend it exists...
return true
end
end
properties[:ensure] == :present
end

Expand Down
210 changes: 207 additions & 3 deletions lib/puppet/type/firewall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
installed.
EOS

feature :hop_limiting, "Hop limiting features."
feature :rate_limiting, "Rate limiting features."
feature :recent_limiting, "The netfilter recent module"
feature :snat, "Source NATing"
feature :dnat, "Destination NATing"
feature :interface_match, "Interface matching"
Expand All @@ -45,6 +47,11 @@
feature :isfragment, "Match fragments"
feature :address_type, "The ability match on source or destination address type"
feature :iprange, "The ability match on source or destination IP range "
feature :ishasmorefrags, "Match a non-last fragment of a fragmented ipv6 packet - might be first"
feature :islastfrag, "Match the last fragment of an ipv6 packet"
feature :isfirstfrag, "Match the first fragment of a fragmented ipv6 packet"
feature :ipsec_policy, "Match IPsec policy"
feature :ipsec_dir, "Match IPsec policy direction"

# provider specific features
feature :iptables, "The provider provides iptables features."
Expand Down Expand Up @@ -103,12 +110,16 @@
source => '192.168.2.0/24'
You can also negate a mask by putting ! in front. For example:
source => '! 192.168.2.0/24'
The source can also be an IPv6 address if your provider supports it.
EOS

munge do |value|
begin
@resource.host_to_ip(value)
@resource.host_to_mask(value)
rescue Exception => e
self.fail("host_to_ip failed for #{value}, exception #{e}")
end
Expand All @@ -134,12 +145,16 @@
destination => '192.168.1.0/24'
You can also negate a mask by putting ! in front. For example:
destination => '! 192.168.2.0/24'
The destination can also be an IPv6 address if your provider supports it.
EOS

munge do |value|
begin
@resource.host_to_ip(value)
@resource.host_to_mask(value)
rescue Exception => e
self.fail("host_to_ip failed for #{value}, exception #{e}")
end
Expand Down Expand Up @@ -441,6 +456,15 @@ def should_to_s(value)
EOS
end

newproperty(:random, :required_features => :dnat) do
desc <<-EOS
When using a jump value of "MASQUERADE", "DNAT", "REDIRECT", or "SNAT"
this boolean will enable randomized port mapping.
EOS

newvalues(:true, :false)
end

# Reject ICMP type
newproperty(:reject, :required_features => :reject_type) do
desc <<-EOS
Expand Down Expand Up @@ -549,6 +573,46 @@ def should_to_s(value)
end
end

newproperty(:ctstate, :array_matching => :all, :required_features =>
:state_match) do

desc <<-EOS
Matches a packet based on its state in the firewall stateful inspection
table, using the conntrack module. Values can be:
* INVALID
* ESTABLISHED
* NEW
* RELATED
EOS

newvalues(:INVALID,:ESTABLISHED,:NEW,:RELATED)

# States should always be sorted. This normalizes the resource states to
# keep it consistent with the sorted result from iptables-save.
def should=(values)
@should = super(values).sort_by {|sym| sym.to_s}
end

def is_to_s(value)
should_to_s(value)
end

def should_to_s(value)
value = [value] unless value.is_a?(Array)
value.join(',')
end
end


# Hop limiting properties
newproperty(:hop_limit, :required_features => :hop_limiting) do
desc <<-EOS
Hop limiting value for matched packets.
EOS
newvalue(/^\d+$/)
end

# Rate limiting properties
newproperty(:limit, :required_features => :rate_limiting) do
desc <<-EOS
Expand Down Expand Up @@ -640,6 +704,104 @@ def should_to_s(value)
newvalues(:true, :false)
end

newproperty(:recent, :required_features => :recent_limiting) do
desc <<-EOS
Enable the recent module. Takes as an argument one of set, update,
rcheck or remove. For example:
# If anyone's appeared on the 'badguy' blacklist within
# the last 60 seconds, drop their traffic, and update the timestamp.
firewall { '100 Drop badguy traffic':
recent => 'update',
rseconds => 60,
rsource => true,
rname => 'badguy',
action => 'DROP',
chain => 'FORWARD',
}
# No-one should be sending us traffic on eth0 from localhost
# Blacklist them
firewall { '101 blacklist strange traffic':
recent => 'set',
rsource => true,
rname => 'badguy',
destination => '127.0.0.0/8',
iniface => 'eth0',
action => 'DROP',
chain => 'FORWARD',
}
EOS

newvalues(:set, :update, :rcheck, :remove)
munge do |value|
value = "--" + value
end
end

newproperty(:rdest, :required_features => :recent_limiting) do
desc <<-EOS
Recent module; add the destination IP address to the list.
Must be boolean true.
EOS

newvalues(:true, :false)
end

newproperty(:rsource, :required_features => :recent_limiting) do
desc <<-EOS
Recent module; add the source IP address to the list.
Must be boolean true.
EOS

newvalues(:true, :false)
end

newproperty(:rname, :required_features => :recent_limiting) do
desc <<-EOS
Recent module; The name of the list. Takes a string argument.
EOS
end

newproperty(:rseconds, :required_features => :recent_limiting) do
desc <<-EOS
Recent module; used in conjunction with one of `recent => 'rcheck'` or
`recent => 'update'`. When used, this will narrow the match to only
happen when the address is in the list and was seen within the last given
number of seconds.
EOS
end

newproperty(:reap, :required_features => :recent_limiting) do
desc <<-EOS
Recent module; can only be used in conjunction with the `rseconds`
attribute. When used, this will cause entries older than 'seconds' to be
purged. Must be boolean true.
EOS
end

newproperty(:rhitcount, :required_features => :recent_limiting) do
desc <<-EOS
Recent module; used in conjunction with `recent => 'update'` or `recent
=> 'rcheck'. When used, this will narrow the match to only happen when
the address is in the list and packets had been received greater than or
equal to the given value.
EOS
end

newproperty(:rttl, :required_features => :recent_limiting) do
desc <<-EOS
Recent module; may only be used in conjunction with one of `recent =>
'rcheck'` or `recent => 'update'`. When used, this will narrow the match
to only happen when the address is in the list and the TTL of the current
packet matches that of the packet which hit the `recent => 'set'` rule.
This may be useful if you have problems with people faking their source
address in order to DoS you via this module by disallowing others access
to your site by sending bogus packets to you. Must be boolean true.
EOS

newvalues(:true, :false)
end

newproperty(:socket, :required_features => :socket) do
desc <<-EOS
If true, matches if an open socket can be found by doing a coket lookup
Expand All @@ -649,6 +811,47 @@ def should_to_s(value)
newvalues(:true, :false)
end

newproperty(:ishasmorefrags, :required_features => :ishasmorefrags) do
desc <<-EOS
If true, matches if the packet has it's 'more fragments' bit set. ipv6.
EOS

newvalues(:true, :false)
end

newproperty(:islastfrag, :required_features => :islastfrag) do
desc <<-EOS
If true, matches if the packet is the last fragment. ipv6.
EOS

newvalues(:true, :false)
end

newproperty(:isfirstfrag, :required_features => :isfirstfrag) do
desc <<-EOS
If true, matches if the packet is the first fragment.
Sadly cannot be negated. ipv6.
EOS

newvalues(:true, :false)
end

newproperty(:ipsec_policy, :required_features => :ipsec_policy) do
desc <<-EOS
Sets the ipsec policy type
EOS

newvalues(:none, :ipsec)
end

newproperty(:ipsec_dir, :required_features => :ipsec_dir) do
desc <<-EOS
Sets the ipsec policy direction
EOS

newvalues(:in, :out)
end

newparam(:line) do
desc <<-EOS
Read-only property for caching the rule line.
Expand All @@ -667,8 +870,9 @@ def should_to_s(value)
end

unless protocol.nil?
table = value(:table)
[value(:chain), value(:jump)].each do |chain|
reqs << "#{chain}:#{value(:table)}:#{protocol}" unless chain.nil?
reqs << "#{chain}:#{table}:#{protocol}" unless ( chain.nil? || (['INPUT', 'OUTPUT', 'FORWARD'].include?(chain) && table == :filter) )
end
end

Expand Down
75 changes: 73 additions & 2 deletions lib/puppet/type/firewallchain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@
raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table 'mangle'"
end
when 'nat'
if chain =~ /^(BROUTING|INPUT|FORWARD)$/
raise ArgumentError, "PREROUTING, POSTROUTING and OUTPUT are the only inbuilt chains that can be used in table 'nat'"
if chain =~ /^(BROUTING|FORWARD)$/
raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, and OUTPUT are the only inbuilt chains that can be used in table 'nat'"
end
if protocol =~/^(IP(v6)?)?$/
raise ArgumentError, "table nat isn't valid in IPv6. You must specify ':IPv4' as the name suffix"
Expand Down Expand Up @@ -105,6 +105,47 @@
end
end

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

newparam(:ignore) do
desc <<-EOS
Regex to perform on firewall rules to exempt unmanaged rules from purging (when enabled).
This is matched against the output of `iptables-save`.
This can be a single regex, or an array of them.
To support flags, use the ruby inline flag mechanism.
Meaning a regex such as
/foo/i
can be written as
'(?i)foo' or '(?i:foo)'
Full example:
firewallchain { 'INPUT:filter:IPv4':
purge => true,
ignore => [
'-j fail2ban-ssh', # ignore the fail2ban jump rule
'--comment "[^"]*(?i:ignore)[^"]*"', # ignore any rules with "ignore" (case insensitive) in the comment in the rule
],
}
EOS

validate do |value|
unless value.is_a?(Array) or value.is_a?(String) or value == false
self.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

# Classes would be a better abstraction, pending:
# http://projects.puppetlabs.com/issues/19001
autorequire(:package) do
Expand Down Expand Up @@ -148,4 +189,34 @@
self.fail 'The "nat" table is not intended for filtering, the use of DROP is therefore inhibited'
end
end

def generate
return [] unless self.purge?

value(:name).match(Nameformat)
chain = $1
table = $2
protocol = $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 or res.provider.properties[:table].to_s != table or 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)

# 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
14 changes: 14 additions & 0 deletions lib/puppet/util/firewall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ def host_to_ip(value)
value.cidr
end

# Takes an address mask 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 host_to_mask(value)
match = value.match /(!)\s?(.*)$/
return host_to_ip(value) unless match

cidr = host_to_ip(match[2])
return nil if cidr == nil
"#{match[1]} #{cidr}"
end

# Validates the argument is int or hex, and returns valid hex
# conversion of the value or nil otherwise.
def to_hex32(value)
Expand Down
29 changes: 29 additions & 0 deletions metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "puppetlabs/firewall",
"version": "0.4.2",
"summary": "Firewall resources (iptables)",
"source": "git@github.com/puppetlabs/puppetlabs-firewall.git",
"project_page": "http://github.com/puppetlabs/puppetlabs-firewall",
"author": "Puppet Labs",
"license": "Apache-2.0",
"operatingsystem_support": [
"RedHat",
"Debian",
"Ubuntu",
"SuSE",
"SLED"
],
"puppet_version": [
2.7,
3.0,
3.1,
3.2,
3.3
],
"dependencies": [
{
"name": "puppetlabs/stdlib",
"version_requirement": ">= 2.2.1"
}
]
}
8 changes: 8 additions & 0 deletions spec/acceptance/basic_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'spec_helper_acceptance'

# Here we put the more basic fundamental tests, ultra obvious stuff.
describe "basic tests:" do
it 'make sure we have copied the module across' do
shell('ls /etc/puppet/modules/firewall/Modulefile', {:acceptable_exit_codes => 0})
end
end
77 changes: 77 additions & 0 deletions spec/acceptance/change_source_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
require 'spec_helper_acceptance'

describe 'firewall type' do
describe 'reset' do
it 'deletes all rules' do
shell('iptables --flush; iptables -t nat --flush; iptables -t mangle --flush')
end
end

describe 'when unmanaged rules exist' do
it 'applies with 8.0.0.1 first' do
pp = <<-EOS
class { '::firewall': }
firewall { '101 test source changes':
proto => tcp,
port => '101',
action => accept,
source => '8.0.0.1',
}
firewall { '100 test source static':
proto => tcp,
port => '100',
action => accept,
source => '8.0.0.2',
}
EOS

apply_manifest(pp, :catch_failures => true)
end

it 'adds a unmanaged rule without a comment' do
shell('/sbin/iptables -A INPUT -t filter -s 8.0.0.3/32 -p tcp -m multiport --ports 102 -j ACCEPT')
expect(shell('iptables -S').stdout).to match(/-A INPUT -s 8\.0\.0\.3\/32 -p tcp -m multiport --ports 102 -j ACCEPT/)
end

it 'contains the changable 8.0.0.1 rule' do
shell('iptables -S') do |r|
expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.1\/32 -p tcp -m multiport --ports 101 -m comment --comment "101 test source changes" -j ACCEPT/)
end
end
it 'contains the static 8.0.0.2 rule' do
shell('iptables -S') do |r|
expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.2\/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test source static" -j ACCEPT/)
end
end

it 'changes to 8.0.0.4 second' do
pp = <<-EOS
class { '::firewall': }
firewall { '101 test source changes':
proto => tcp,
port => '101',
action => accept,
source => '8.0.0.4',
}
EOS

expect(apply_manifest(pp, :catch_failures => true).stdout).to match(/Notice: \/Stage\[main\]\/Main\/Firewall\[101 test source changes\]\/source: source changed '8\.0\.0\.1\/32' to '8\.0\.0\.4\/32'/)
end

it 'does not contain the old changing 8.0.0.1 rule' do
shell('iptables -S') do |r|
expect(r.stdout).to_not match(/8\.0\.0\.1/)
end
end
it 'contains the staic 8.0.0.2 rule' do
shell('iptables -S') do |r|
expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.2\/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test source static" -j ACCEPT/)
end
end
it 'contains the changing new 8.0.0.4 rule' do
shell('iptables -S') do |r|
expect(r.stdout).to match(/-A INPUT -s 8\.0\.0\.4\/32 -p tcp -m multiport --ports 101 -m comment --comment "101 test source changes" -j ACCEPT/)
end
end
end
end
27 changes: 27 additions & 0 deletions spec/acceptance/class_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require 'spec_helper_acceptance'

describe "firewall class:" do
it 'should run successfully' do
pp = "class { 'firewall': }"

# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero
end

it 'ensure => stopped:' do
pp = "class { 'firewall': ensure => stopped }"

# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero
end

it 'ensure => running:' do
pp = "class { 'firewall': ensure => running }"

# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero
end
end
1,608 changes: 1,608 additions & 0 deletions spec/acceptance/firewall_spec.rb

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions spec/acceptance/firewallchain_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
require 'spec_helper_acceptance'

describe 'puppet resource firewallchain command:' do
before :all do
iptables_flush_all_tables
end
describe 'ensure' do
context 'present' do
it 'applies cleanly' do
pp = <<-EOS
firewallchain { 'MY_CHAIN:filter:IPv4':
ensure => present,
}
EOS
# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)
end

it 'finds the chain' do
shell('iptables -S') do |r|
expect(r.stdout).to match(/-N MY_CHAIN/)
end
end
end

context 'absent' do
it 'applies cleanly' do
pp = <<-EOS
firewallchain { 'MY_CHAIN:filter:IPv4':
ensure => absent,
}
EOS
# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)
end

it 'fails to find the chain' do
shell('iptables -S') do |r|
expect(r.stdout).to_not match(/-N MY_CHAIN/)
end
end
end
end

# XXX purge => false is not yet implemented
#context 'adding a firewall rule to a chain:' do
# it 'applies cleanly' do
# pp = <<-EOS
# firewallchain { 'MY_CHAIN:filter:IPv4':
# ensure => present,
# }
# firewall { '100 my rule':
# chain => 'MY_CHAIN',
# action => 'accept',
# proto => 'tcp',
# dport => 5000,
# }
# EOS
# # Run it twice and test for idempotency
# apply_manifest(pp, :catch_failures => true)
# apply_manifest(pp, :catch_changes => true)
# end
#end

#context 'not purge firewallchain chains:' do
# it 'does not purge the rule' do
# pp = <<-EOS
# firewallchain { 'MY_CHAIN:filter:IPv4':
# ensure => present,
# purge => false,
# before => Resources['firewall'],
# }
# resources { 'firewall':
# purge => true,
# }
# EOS
# # Run it twice and test for idempotency
# apply_manifest(pp, :catch_failures => true) do |r|
# expect(r.stdout).to_not match(/removed/)
# expect(r.stderr).to eq('')
# end
# apply_manifest(pp, :catch_changes => true)
# end

# it 'still has the rule' do
# pp = <<-EOS
# firewall { '100 my rule':
# chain => 'MY_CHAIN',
# action => 'accept',
# proto => 'tcp',
# dport => 5000,
# }
# EOS
# # Run it twice and test for idempotency
# apply_manifest(pp, :catch_changes => true)
# end
#end

describe 'policy' do
after :all do
shell('iptables -t filter -P FORWARD ACCEPT')
end

context 'DROP' do
it 'applies cleanly' do
pp = <<-EOS
firewallchain { 'FORWARD:filter:IPv4':
policy => 'drop',
}
EOS
# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)
end

it 'finds the chain' do
shell('iptables -S') do |r|
expect(r.stdout).to match(/-P FORWARD DROP/)
end
end
end
end
end
94 changes: 94 additions & 0 deletions spec/acceptance/ip6_fragment_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
require 'spec_helper_acceptance'

describe 'firewall ishasmorefrags/islastfrag/isfirstfrag properties' do
before :all do
ip6tables_flush_all_tables
end

shared_examples "is idempotent" do |values, line_match|
it "changes the values to #{values}" do
pp = <<-EOS
class { '::firewall': }
firewall { '599 - test':
ensure => present,
proto => 'tcp',
provider => 'ip6tables',
#{values}
}
EOS

apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)

shell('ip6tables -S') do |r|
expect(r.stdout).to match(/#{line_match}/)
end
end
end
shared_examples "doesn't change" do |values, line_match|
it "doesn't change the values to #{values}" do
pp = <<-EOS
class { '::firewall': }
firewall { '599 - test':
ensure => present,
proto => 'tcp',
provider => 'ip6tables',
#{values}
}
EOS

apply_manifest(pp, :catch_changes => true)

shell('ip6tables -S') do |r|
expect(r.stdout).to match(/#{line_match}/)
end
end
end

describe 'adding a rule' do
context 'when unset' do
before :all do
ip6tables_flush_all_tables
end
it_behaves_like 'is idempotent', '', /-A INPUT -p tcp -m comment --comment "599 - test"/
end
context 'when set to true' do
before :all do
ip6tables_flush_all_tables
end
it_behaves_like "is idempotent", 'ishasmorefrags => true, islastfrag => true, isfirstfrag => true', /-A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"/
end
context 'when set to false' do
before :all do
ip6tables_flush_all_tables
end
it_behaves_like "is idempotent", 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', /-A INPUT -p tcp -m comment --comment "599 - test"/
end
end
describe 'editing a rule' do
context 'when unset or false' do
before :each do
ip6tables_flush_all_tables
shell('/sbin/ip6tables -A INPUT -p tcp -m comment --comment "599 - test"')
end
context 'and current value is false' do
it_behaves_like "doesn't change", 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', /-A INPUT -p tcp -m comment --comment "599 - test"/
end
context 'and current value is true' do
it_behaves_like "is idempotent", 'ishasmorefrags => true, islastfrag => true, isfirstfrag => true', /-A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"/
end
end
context 'when set to true' do
before :each do
ip6tables_flush_all_tables
shell('/sbin/ip6tables -A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"')
end
context 'and current value is false' do
it_behaves_like "is idempotent", 'ishasmorefrags => false, islastfrag => false, isfirstfrag => false', /-A INPUT -p tcp -m comment --comment "599 - test"/
end
context 'and current value is true' do
it_behaves_like "doesn't change", 'ishasmorefrags => true, islastfrag => true, isfirstfrag => true', /-A INPUT -p tcp -m frag --fragid 0 --fragmore -m frag --fragid 0 --fraglast -m frag --fragid 0 --fragfirst -m comment --comment "599 - test"/
end
end
end
end
92 changes: 92 additions & 0 deletions spec/acceptance/isfragment_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
require 'spec_helper_acceptance'

describe 'firewall isfragment property' do
before :all do
iptables_flush_all_tables
end

shared_examples "is idempotent" do |value, line_match|
it "changes the value to #{value}" do
pp = <<-EOS
class { '::firewall': }
firewall { '597 - test':
ensure => present,
proto => 'tcp',
#{value}
}
EOS

apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)

shell('iptables -S') do |r|
expect(r.stdout).to match(/#{line_match}/)
end
end
end
shared_examples "doesn't change" do |value, line_match|
it "doesn't change the value to #{value}" do
pp = <<-EOS
class { '::firewall': }
firewall { '597 - test':
ensure => present,
proto => 'tcp',
#{value}
}
EOS

apply_manifest(pp, :catch_changes => true)

shell('iptables -S') do |r|
expect(r.stdout).to match(/#{line_match}/)
end
end
end

describe 'adding a rule' do
context 'when unset' do
before :all do
iptables_flush_all_tables
end
it_behaves_like 'is idempotent', '', /-A INPUT -p tcp -m comment --comment "597 - test"/
end
context 'when set to true' do
before :all do
iptables_flush_all_tables
end
it_behaves_like 'is idempotent', 'isfragment => true,', /-A INPUT -p tcp -f -m comment --comment "597 - test"/
end
context 'when set to false' do
before :all do
iptables_flush_all_tables
end
it_behaves_like "is idempotent", 'isfragment => false,', /-A INPUT -p tcp -m comment --comment "597 - test"/
end
end
describe 'editing a rule' do
context 'when unset or false' do
before :each do
iptables_flush_all_tables
shell('/sbin/iptables -A INPUT -p tcp -m comment --comment "597 - test"')
end
context 'and current value is false' do
it_behaves_like "doesn't change", 'isfragment => false,', /-A INPUT -p tcp -m comment --comment "597 - test"/
end
context 'and current value is true' do
it_behaves_like "is idempotent", 'isfragment => true,', /-A INPUT -p tcp -f -m comment --comment "597 - test"/
end
end
context 'when set to true' do
before :each do
iptables_flush_all_tables
shell('/sbin/iptables -A INPUT -p tcp -f -m comment --comment "597 - test"')
end
context 'and current value is false' do
it_behaves_like "is idempotent", 'isfragment => false,', /-A INPUT -p tcp -m comment --comment "597 - test"/
end
context 'and current value is true' do
it_behaves_like "doesn't change", 'isfragment => true,', /-A INPUT -p tcp -f -m comment --comment "597 - test"/
end
end
end
end
10 changes: 10 additions & 0 deletions spec/acceptance/nodesets/centos-59-x64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HOSTS:
centos-59-x64:
roles:
- master
platform: el-5-x86_64
box : centos-59-x64-vbox4210-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-59-x64-vbox4210-nocm.box
hypervisor : vagrant
CONFIG:
type: foss
10 changes: 10 additions & 0 deletions spec/acceptance/nodesets/centos-64-x64-fusion.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HOSTS:
centos-64-x64:
roles:
- master
platform: el-6-x86_64
box : centos-64-x64-fusion503-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-fusion503-nocm.box
hypervisor : fusion
CONFIG:
type: foss
12 changes: 12 additions & 0 deletions spec/acceptance/nodesets/centos-64-x64-pe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
HOSTS:
centos-64-x64:
roles:
- master
- database
- dashboard
platform: el-6-x86_64
box : centos-64-x64-vbox4210-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box
hypervisor : vagrant
CONFIG:
type: pe
10 changes: 10 additions & 0 deletions spec/acceptance/nodesets/centos-64-x64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HOSTS:
centos-64-x64:
roles:
- master
platform: el-6-x86_64
box : centos-64-x64-vbox4210-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/centos-64-x64-vbox4210-nocm.box
hypervisor : vagrant
CONFIG:
type: foss
10 changes: 10 additions & 0 deletions spec/acceptance/nodesets/debian-607-x64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HOSTS:
debian-607-x64:
roles:
- master
platform: debian-6-amd64
box : debian-607-x64-vbox4210-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-607-x64-vbox4210-nocm.box
hypervisor : vagrant
CONFIG:
type: git
10 changes: 10 additions & 0 deletions spec/acceptance/nodesets/debian-70rc1-x64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HOSTS:
debian-70rc1-x64:
roles:
- master
platform: debian-7-amd64
box : debian-70rc1-x64-vbox4210-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/debian-70rc1-x64-vbox4210-nocm.box
hypervisor : vagrant
CONFIG:
type: git
1 change: 1 addition & 0 deletions spec/acceptance/nodesets/default.yml
10 changes: 10 additions & 0 deletions spec/acceptance/nodesets/fedora-18-x64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HOSTS:
fedora-18-x64:
roles:
- master
platform: fedora-18-x86_64
box : fedora-18-x64-vbox4210-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/fedora-18-x64-vbox4210-nocm.box
hypervisor : vagrant
CONFIG:
type: git
10 changes: 10 additions & 0 deletions spec/acceptance/nodesets/sles-11sp1-x64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HOSTS:
sles-11sp1-x64:
roles:
- master
platform: sles-11-x86_64
box : sles-11sp1-x64-vbox4210-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/sles-11sp1-x64-vbox4210-nocm.box
hypervisor : vagrant
CONFIG:
type: git
10 changes: 10 additions & 0 deletions spec/acceptance/nodesets/ubuntu-server-10044-x64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HOSTS:
ubuntu-server-10044-x64:
roles:
- master
platform: ubuntu-10.04-amd64
box : ubuntu-server-10044-x64-vbox4210-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-10044-x64-vbox4210-nocm.box
hypervisor : vagrant
CONFIG:
type: git
10 changes: 10 additions & 0 deletions spec/acceptance/nodesets/ubuntu-server-12042-x64.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
HOSTS:
ubuntu-server-12042-x64:
roles:
- master
platform: ubuntu-12.04-amd64
box : ubuntu-server-12042-x64-vbox4210-nocm
box_url : http://puppet-vagrant-boxes.puppetlabs.com/ubuntu-server-12042-x64-vbox4210-nocm.box
hypervisor : vagrant
CONFIG:
type: foss
96 changes: 44 additions & 52 deletions spec/system/params_spec.rb → spec/acceptance/params_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require 'spec_helper_system'
require 'spec_helper_acceptance'

describe "param based tests:" do
# Takes a hash and converts it into a firewall resource
Expand All @@ -8,7 +8,7 @@ def pp(params)
firewall { '#{name}':
EOS

params.each do |k,v|
params.each do |k,v|
pm += <<-EOS
#{k} => #{v},
EOS
Expand All @@ -23,10 +23,8 @@ def pp(params)
it 'test various params' do
iptables_flush_all_tables

facts = node.facts

unless (facts['operatingsystem'] == 'CentOS') && \
facts['operatingsystemrelease'] =~ /^5\./ then
unless (fact('operatingsystem') == 'CentOS') && \
fact('operatingsystemrelease') =~ /^5\./ then

ppm = pp({
'table' => "'raw'",
Expand All @@ -36,13 +34,8 @@ def pp(params)
'log_level' => 'debug',
})

puppet_apply(ppm) do |r|
r.exit_code.should == 2
r.stderr.should be_empty
r.refresh
r.stderr.should be_empty
r.exit_code.should be_zero
end
expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2)
expect(apply_manifest(ppm, :catch_failures => true).exit_code).to be_zero
end
end

Expand All @@ -55,13 +48,8 @@ def pp(params)
'jump' => 'LOG',
'log_level' => 'debug',
})
puppet_apply(ppm) do |r|
r.exit_code.should == 2
r.stderr.should be_empty
r.refresh
r.stderr.should be_empty
r.exit_code.should be_zero
end
expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2)
expect(apply_manifest(ppm, :catch_failures => true).exit_code).to be_zero
end

it 'test log rule - changing names' do
Expand All @@ -71,7 +59,7 @@ def pp(params)
'name' => '004 log all INVALID packets',
'chain' => 'INPUT',
'proto' => 'all',
'state' => 'INVALID',
'ctstate' => 'INVALID',
'jump' => 'LOG',
'log_level' => '3',
'log_prefix' => '"IPTABLES dropped invalid: "',
Expand All @@ -81,26 +69,45 @@ def pp(params)
'name' => '003 log all INVALID packets',
'chain' => 'INPUT',
'proto' => 'all',
'state' => 'INVALID',
'ctstate' => 'INVALID',
'jump' => 'LOG',
'log_level' => '3',
'log_prefix' => '"IPTABLES dropped invalid: "',
})

puppet_apply(ppm1) do |r|
r.stderr.should be_empty
r.exit_code.should == 2
end
expect(apply_manifest(ppm1, :catch_failures => true).exit_code).to eq(2)

ppm = <<-EOS + "\n" + ppm2
resources { 'firewall':
purge => true,
}
EOS
puppet_apply(ppm) do |r|
r.stderr.should be_empty
r.exit_code.should == 2
end
expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2)
end

it 'test chain - changing names' do
iptables_flush_all_tables

ppm1 = pp({
'name' => '004 with a chain',
'chain' => 'INPUT',
'proto' => 'all',
})

ppm2 = pp({
'name' => '004 with a chain',
'chain' => 'OUTPUT',
'proto' => 'all',
})

apply_manifest(ppm1, :expect_changes => true)

ppm = <<-EOS + "\n" + ppm2
resources { 'firewall':
purge => true,
}
EOS
expect(apply_manifest(ppm2, :expect_failures => true).stderr).to match(/is not supported/)
end

it 'test log rule - idempotent' do
Expand All @@ -110,19 +117,14 @@ def pp(params)
'name' => '004 log all INVALID packets',
'chain' => 'INPUT',
'proto' => 'all',
'state' => 'INVALID',
'ctstate' => 'INVALID',
'jump' => 'LOG',
'log_level' => '3',
'log_prefix' => '"IPTABLES dropped invalid: "',
})

puppet_apply(ppm1) do |r|
r.exit_code.should == 2
r.stderr.should be_empty
r.refresh
r.stderr.should be_empty
r.exit_code.should be_zero
end
expect(apply_manifest(ppm1, :catch_failures => true).exit_code).to eq(2)
expect(apply_manifest(ppm1, :catch_failures => true).exit_code).to be_zero
end

it 'test src_range rule' do
Expand All @@ -135,13 +137,8 @@ def pp(params)
'action' => 'drop',
'src_range' => '"10.0.0.1-10.0.0.10"',
})
puppet_apply(ppm) do |r|
r.exit_code.should == 2
r.stderr.should be_empty
r.refresh
r.stderr.should be_empty
r.exit_code.should be_zero
end
expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2)
expect(apply_manifest(ppm, :catch_failures => true).exit_code).to be_zero
end

it 'test dst_range rule' do
Expand All @@ -154,13 +151,8 @@ def pp(params)
'action' => 'drop',
'dst_range' => '"10.0.0.2-10.0.0.20"',
})
puppet_apply(ppm) do |r|
r.exit_code.should == 2
r.stderr.should be_empty
r.refresh
r.stderr.should be_empty
r.exit_code.should be_zero
end
expect(apply_manifest(ppm, :catch_failures => true).exit_code).to eq(2)
expect(apply_manifest(ppm, :catch_failures => true).exit_code).to be_zero
end

end
124 changes: 124 additions & 0 deletions spec/acceptance/purge_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
require 'spec_helper_acceptance'

describe "purge tests:" do
context('resources purge') do
before(:all) do
iptables_flush_all_tables

shell('/sbin/iptables -A INPUT -s 1.2.1.2')
shell('/sbin/iptables -A INPUT -s 1.2.1.2')
end

it 'make sure duplicate existing rules get purged' do

pp = <<-EOS
class { 'firewall': }
resources { 'firewall':
purge => true,
}
EOS

apply_manifest(pp, :expect_changes => true)
end

it 'saves' do
shell('/sbin/iptables-save') do |r|
expect(r.stdout).to_not match(/1\.2\.1\.2/)
expect(r.stderr).to eq("")
end
end
end

context('chain purge') do
before(:each) do
iptables_flush_all_tables

shell('/sbin/iptables -A INPUT -p tcp -s 1.2.1.1')
shell('/sbin/iptables -A INPUT -p udp -s 1.2.1.1')
shell('/sbin/iptables -A OUTPUT -s 1.2.1.2 -m comment --comment "010 output-1.2.1.2"')
end

it 'purges only the specified chain' do
pp = <<-EOS
class { 'firewall': }
firewallchain { 'INPUT:filter:IPv4':
purge => true,
}
EOS

apply_manifest(pp, :expect_changes => true)

shell('/sbin/iptables-save') do |r|
expect(r.stdout).to match(/010 output-1\.2\.1\.2/)
expect(r.stdout).to_not match(/1\.2\.1\.1/)
expect(r.stderr).to eq("")
end
end

it 'ignores managed rules' do
pp = <<-EOS
class { 'firewall': }
firewallchain { 'OUTPUT:filter:IPv4':
purge => true,
}
firewall { '010 output-1.2.1.2':
chain => 'OUTPUT',
proto => 'all',
source => '1.2.1.2',
}
EOS

apply_manifest(pp, :catch_changes => true)
end

it 'ignores specified rules' do
pp = <<-EOS
class { 'firewall': }
firewallchain { 'INPUT:filter:IPv4':
purge => true,
ignore => [
'-s 1\.2\.1\.1',
],
}
EOS

apply_manifest(pp, :catch_changes => true)
end

it 'adds managed rules with ignored rules' do
pp = <<-EOS
class { 'firewall': }
firewallchain { 'INPUT:filter:IPv4':
purge => true,
ignore => [
'-s 1\.2\.1\.1',
],
}
firewall { '014 input-1.2.1.6':
chain => 'INPUT',
proto => 'all',
source => '1.2.1.6',
}
-> firewall { '013 input-1.2.1.5':
chain => 'INPUT',
proto => 'all',
source => '1.2.1.5',
}
-> firewall { '012 input-1.2.1.4':
chain => 'INPUT',
proto => 'all',
source => '1.2.1.4',
}
-> firewall { '011 input-1.2.1.3':
chain => 'INPUT',
proto => 'all',
source => '1.2.1.3',
}
EOS

apply_manifest(pp, :catch_failures => true)

expect(shell('/sbin/iptables-save').stdout).to match(/-A INPUT -s 1\.2\.1\.1\/32 -p tcp \n-A INPUT -s 1\.2\.1\.1\/32 -p udp/)
end
end
end
93 changes: 93 additions & 0 deletions spec/acceptance/resource_cmd_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
require 'spec_helper_acceptance'

# Here we want to test the the resource commands ability to work with different
# existing ruleset scenarios. This will give the parsing capabilities of the
# code a good work out.
describe 'puppet resource firewall command:' do
context 'make sure it returns no errors when executed on a clean machine' do
it do
shell('puppet resource firewall') do |r|
r.exit_code.should be_zero
# don't check stdout, some boxes come with rules, that is normal
r.stderr.should be_empty
end
end
end

context 'flush iptables and make sure it returns nothing afterwards' do
before(:all) do
iptables_flush_all_tables
end

# No rules, means no output thanks. And no errors as well.
it do
shell('puppet resource firewall') do |r|
r.exit_code.should be_zero
r.stderr.should be_empty
r.stdout.should == "\n"
end
end
end

context 'accepts rules without comments' do
before(:all) do
iptables_flush_all_tables
shell('/sbin/iptables -A INPUT -j ACCEPT -p tcp --dport 80')
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 'accepts rules with invalid comments' do
before(:all) do
iptables_flush_all_tables
shell('/sbin/iptables -A INPUT -j ACCEPT -p tcp --dport 80 -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 'accepts rules with negation' do
before :all do
iptables_flush_all_tables
shell('/sbin/iptables -t nat -A POSTROUTING -s 192.168.122.0/24 ! -d 192.168.122.0/24 -p tcp -j MASQUERADE --to-ports 1024-65535')
shell('/sbin/iptables -t nat -A POSTROUTING -s 192.168.122.0/24 ! -d 192.168.122.0/24 -p udp -j MASQUERADE --to-ports 1024-65535')
shell('/sbin/iptables -t nat -A POSTROUTING -s 192.168.122.0/24 ! -d 192.168.122.0/24 -j MASQUERADE')
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 'accepts rules with match extension tcp flag' do
before :all do
iptables_flush_all_tables
shell('/sbin/iptables -t mangle -A PREROUTING -d 1.2.3.4 -p tcp -m tcp -m multiport --dports 80,443,8140 -j MARK --set-mark 42')
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
end
248 changes: 248 additions & 0 deletions spec/acceptance/rules_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
require 'spec_helper_acceptance'

describe 'complex ruleset 1' do
before :all do
iptables_flush_all_tables
end

after :all do
shell('iptables -t filter -P INPUT ACCEPT')
shell('iptables -t filter -P FORWARD ACCEPT')
shell('iptables -t filter -P OUTPUT ACCEPT')
shell('iptables -t filter --flush')
end

it 'applies cleanly' do
pp = <<-EOS
firewall { '090 forward allow local':
chain => 'FORWARD',
proto => 'all',
source => '10.0.0.0/8',
destination => '10.0.0.0/8',
action => 'accept',
}
firewall { '100 forward standard allow tcp':
chain => 'FORWARD',
source => '10.0.0.0/8',
destination => '!10.0.0.0/8',
proto => 'tcp',
state => 'NEW',
port => [80,443,21,20,22,53,123,43,873,25,465],
action => 'accept',
}
firewall { '100 forward standard allow udp':
chain => 'FORWARD',
source => '10.0.0.0/8',
destination => '!10.0.0.0/8',
proto => 'udp',
port => [53,123],
action => 'accept',
}
firewall { '100 forward standard allow icmp':
chain => 'FORWARD',
source => '10.0.0.0/8',
destination => '!10.0.0.0/8',
proto => 'icmp',
action => 'accept',
}
firewall { '090 ignore ipsec':
table => 'nat',
chain => 'POSTROUTING',
outiface => 'eth0',
ipsec_policy => 'ipsec',
ipsec_dir => 'out',
action => 'accept',
}
firewall { '093 ignore 10.0.0.0/8':
table => 'nat',
chain => 'POSTROUTING',
outiface => 'eth0',
destination => '10.0.0.0/8',
action => 'accept',
}
firewall { '093 ignore 172.16.0.0/12':
table => 'nat',
chain => 'POSTROUTING',
outiface => 'eth0',
destination => '172.16.0.0/12',
action => 'accept',
}
firewall { '093 ignore 192.168.0.0/16':
table => 'nat',
chain => 'POSTROUTING',
outiface => 'eth0',
destination => '192.168.0.0/16',
action => 'accept',
}
firewall { '100 masq outbound':
table => 'nat',
chain => 'POSTROUTING',
outiface => 'eth0',
jump => 'MASQUERADE',
}
firewall { '101 redirect port 1':
table => 'nat',
chain => 'PREROUTING',
iniface => 'eth0',
proto => 'tcp',
dport => '1',
toports => '22',
jump => 'REDIRECT',
}
EOS

# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero
end

it 'contains appropriate rules' do
shell('iptables -S') do |r|
expect(r.stdout).to eq(
"-P INPUT ACCEPT\n" +
"-P FORWARD ACCEPT\n" +
"-P OUTPUT ACCEPT\n" +
"-A FORWARD -s 10.0.0.0/8 -d 10.0.0.0/8 -m comment --comment \"090 forward allow local\" -j ACCEPT \n" +
"-A FORWARD -s 10.0.0.0/8 ! -d 10.0.0.0/8 -p icmp -m comment --comment \"100 forward standard allow icmp\" -j ACCEPT \n" +
"-A FORWARD -s 10.0.0.0/8 ! -d 10.0.0.0/8 -p tcp -m multiport --ports 80,443,21,20,22,53,123,43,873,25,465 -m comment --comment \"100 forward standard allow tcp\" -m state --state NEW -j ACCEPT \n" +
"-A FORWARD -s 10.0.0.0/8 ! -d 10.0.0.0/8 -p udp -m multiport --ports 53,123 -m comment --comment \"100 forward standard allow udp\" -j ACCEPT \n"
)
end
end
end

describe 'complex ruleset 2' do
after :all do
shell('iptables -t filter -P INPUT ACCEPT')
shell('iptables -t filter -P FORWARD ACCEPT')
shell('iptables -t filter -P OUTPUT ACCEPT')
shell('iptables -t filter --flush')
expect(shell('iptables -t filter -X LOCAL_INPUT').stderr).to eq("")
expect(shell('iptables -t filter -X LOCAL_INPUT_PRE').stderr).to eq("")
end

it 'applies cleanly' do
pp = <<-EOS
class { '::firewall': }
Firewall {
proto => 'all',
stage => 'pre',
}
Firewallchain {
stage => 'pre',
purge => 'true',
ignore => [
'--comment "[^"]*(?i:ignore)[^"]*"',
],
}
firewall { '010 INPUT allow established and related':
proto => 'all',
state => ['ESTABLISHED', 'RELATED'],
action => 'accept',
before => Firewallchain['INPUT:filter:IPv4'],
}
firewall { '012 accept loopback':
iniface => 'lo',
action => 'accept',
before => Firewallchain['INPUT:filter:IPv4'],
}
firewall { '020 ssh':
proto => 'tcp',
dport => '22',
state => 'NEW',
action => 'accept',
before => Firewallchain['INPUT:filter:IPv4'],
}
firewall { '013 icmp echo-request':
proto => 'icmp',
icmp => 'echo-request',
action => 'accept',
source => '10.0.0.0/8',
}
firewall { '013 icmp destination-unreachable':
proto => 'icmp',
icmp => 'destination-unreachable',
action => 'accept',
}
firewall { '013 icmp time-exceeded':
proto => 'icmp',
icmp => 'time-exceeded',
action => 'accept',
}
firewall { '999 reject':
action => 'reject',
reject => 'icmp-host-prohibited',
}
firewallchain { 'LOCAL_INPUT_PRE:filter:IPv4': }
firewall { '001 LOCAL_INPUT_PRE':
jump => 'LOCAL_INPUT_PRE',
require => Firewallchain['LOCAL_INPUT_PRE:filter:IPv4'],
}
firewallchain { 'LOCAL_INPUT:filter:IPv4': }
firewall { '900 LOCAL_INPUT':
jump => 'LOCAL_INPUT',
require => Firewallchain['LOCAL_INPUT:filter:IPv4'],
}
firewallchain { 'INPUT:filter:IPv4':
policy => 'drop',
ignore => [
'-j fail2ban-ssh',
'--comment "[^"]*(?i:ignore)[^"]*"',
],
}
firewall { '010 allow established and related':
chain => 'FORWARD',
proto => 'all',
state => ['ESTABLISHED','RELATED'],
action => 'accept',
before => Firewallchain['FORWARD:filter:IPv4'],
}
firewallchain { 'FORWARD:filter:IPv4':
policy => 'drop',
}
firewallchain { 'OUTPUT:filter:IPv4': }
# purge unknown rules from mangle table
firewallchain { ['PREROUTING:mangle:IPv4', 'INPUT:mangle:IPv4', 'FORWARD:mangle:IPv4', 'OUTPUT:mangle:IPv4', 'POSTROUTING:mangle:IPv4']: }
# and the nat table
firewallchain { ['PREROUTING:nat:IPv4', 'INPUT:nat:IPv4', 'OUTPUT:nat:IPv4', 'POSTROUTING:nat:IPv4']: }
EOS

# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)
end

it 'contains appropriate rules' do
shell('iptables -S') do |r|
expect(r.stdout).to eq(
"-P INPUT DROP\n" +
"-P FORWARD DROP\n" +
"-P OUTPUT ACCEPT\n" +
"-N LOCAL_INPUT\n" +
"-N LOCAL_INPUT_PRE\n" +
"-A INPUT -m comment --comment \"001 LOCAL_INPUT_PRE\" -j LOCAL_INPUT_PRE \n" +
"-A INPUT -m comment --comment \"010 INPUT allow established and related\" -m state --state RELATED,ESTABLISHED -j ACCEPT \n" +
"-A INPUT -i lo -m comment --comment \"012 accept loopback\" -j ACCEPT \n" +
"-A INPUT -p icmp -m comment --comment \"013 icmp destination-unreachable\" -m icmp --icmp-type 3 -j ACCEPT \n" +
"-A INPUT -s 10.0.0.0/8 -p icmp -m comment --comment \"013 icmp echo-request\" -m icmp --icmp-type 8 -j ACCEPT \n" +
"-A INPUT -p icmp -m comment --comment \"013 icmp time-exceeded\" -m icmp --icmp-type 11 -j ACCEPT \n" +
"-A INPUT -p tcp -m multiport --dports 22 -m comment --comment \"020 ssh\" -m state --state NEW -j ACCEPT \n" +
"-A INPUT -m comment --comment \"900 LOCAL_INPUT\" -j LOCAL_INPUT \n" +
"-A INPUT -m comment --comment \"999 reject\" -j REJECT --reject-with icmp-host-prohibited \n" +
"-A FORWARD -m comment --comment \"010 allow established and related\" -m state --state RELATED,ESTABLISHED -j ACCEPT \n"
)
end
end
end
96 changes: 96 additions & 0 deletions spec/acceptance/socket_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
require 'spec_helper_acceptance'

describe 'firewall socket property' do
before :all do
iptables_flush_all_tables
end

shared_examples "is idempotent" do |value, line_match|
it "changes the value to #{value}" do
pp = <<-EOS
class { '::firewall': }
firewall { '598 - test':
ensure => present,
proto => 'tcp',
chain => 'PREROUTING',
table => 'raw',
#{value}
}
EOS

apply_manifest(pp, :catch_failures => true)
apply_manifest(pp, :catch_changes => true)

shell('iptables -t raw -S') do |r|
expect(r.stdout).to match(/#{line_match}/)
end
end
end
shared_examples "doesn't change" do |value, line_match|
it "doesn't change the value to #{value}" do
pp = <<-EOS
class { '::firewall': }
firewall { '598 - test':
ensure => present,
proto => 'tcp',
chain => 'PREROUTING',
table => 'raw',
#{value}
}
EOS

apply_manifest(pp, :catch_changes => true)

shell('iptables -t raw -S') do |r|
expect(r.stdout).to match(/#{line_match}/)
end
end
end

describe 'adding a rule' do
context 'when unset' do
before :all do
iptables_flush_all_tables
end
it_behaves_like 'is idempotent', '', /-A PREROUTING -p tcp -m comment --comment "598 - test"/
end
context 'when set to true' do
before :all do
iptables_flush_all_tables
end
it_behaves_like 'is idempotent', 'socket => true,', /-A PREROUTING -p tcp -m socket -m comment --comment "598 - test"/
end
context 'when set to false' do
before :all do
iptables_flush_all_tables
end
it_behaves_like "is idempotent", 'socket => false,', /-A PREROUTING -p tcp -m comment --comment "598 - test"/
end
end
describe 'editing a rule' do
context 'when unset or false' do
before :each do
iptables_flush_all_tables
shell('/sbin/iptables -t raw -A PREROUTING -p tcp -m comment --comment "598 - test"')
end
context 'and current value is false' do
it_behaves_like "doesn't change", 'socket => false,', /-A PREROUTING -p tcp -m comment --comment "598 - test"/
end
context 'and current value is true' do
it_behaves_like "is idempotent", 'socket => true,', /-A PREROUTING -p tcp -m socket -m comment --comment "598 - test"/
end
end
context 'when set to true' do
before :each do
iptables_flush_all_tables
shell('/sbin/iptables -t raw -A PREROUTING -p tcp -m socket -m comment --comment "598 - test"')
end
context 'and current value is false' do
it_behaves_like "is idempotent", 'socket => false,', /-A PREROUTING -p tcp -m comment --comment "598 - test"/
end
context 'and current value is true' do
it_behaves_like "doesn't change", 'socket => true,', /-A PREROUTING -p tcp -m socket -m comment --comment "598 - test"/
end
end
end
end
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
require 'spec_helper_system'
require 'spec_helper_acceptance'

# Some tests for the standard recommended usage
describe 'standard usage tests:' do
context 'standard 1' do
it 'applies twice' do
pp = <<-EOS
class my_fw::pre {
Firewall {
Expand All @@ -21,7 +21,7 @@ class my_fw::pre {
}->
firewall { '002 accept related established rules':
proto => 'all',
state => ['RELATED', 'ESTABLISHED'],
ctstate => ['RELATED', 'ESTABLISHED'],
action => 'accept',
}
}
Expand All @@ -48,12 +48,8 @@ class { 'firewall': }
}
EOS

context puppet_apply(pp) do
its(:stderr) { should be_empty }
its(:exit_code) { should_not == 1 }
its(:refresh) { should be_nil }
its(:stderr) { should be_empty }
its(:exit_code) { should be_zero }
end
# Run it twice and test for idempotency
apply_manifest(pp, :catch_failures => true)
expect(apply_manifest(pp, :catch_failures => true).exit_code).to be_zero
end
end
107 changes: 107 additions & 0 deletions spec/fixtures/ip6tables/conversion_hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# These hashes allow us to iterate across a series of test data
# creating rspec examples for each parameter to ensure the input :line
# extrapolates to the desired value for the parameter in question. And
# vice-versa

# This hash is for testing a line conversion to a hash of parameters
# which will be used to create a resource.
ARGS_TO_HASH6 = {
'source_destination_ipv6_no_cidr' => {
:line => '-A INPUT -s 2001:db8:85a3::8a2e:370:7334 -d 2001:db8:85a3::8a2e:370:7334 -m comment --comment "000 source destination ipv6 no cidr"',
:table => 'filter',
:provider => 'ip6tables',
:params => {
:source => '2001:db8:85a3::8a2e:370:7334/128',
:destination => '2001:db8:85a3::8a2e:370:7334/128',
},
},
'source_destination_ipv6_netmask' => {
:line => '-A INPUT -s 2001:db8:1234::/ffff:ffff:ffff:0000:0000:0000:0000:0000 -d 2001:db8:4321::/ffff:ffff:ffff:0000:0000:0000:0000:0000 -m comment --comment "000 source destination ipv6 netmask"',
:table => 'filter',
:provider => 'ip6tables',
:params => {
:source => '2001:db8:1234::/48',
:destination => '2001:db8:4321::/48',
},
},
}

# This hash is for testing converting a hash to an argument line.
HASH_TO_ARGS6 = {
'zero_prefixlen_ipv6' => {
:params => {
:name => '100 zero prefix length ipv6',
:table => 'filter',
:provider => 'ip6tables',
:source => '::/0',
:destination => '::/0',
},
:args => ['-t', :filter, '-p', :tcp, '-m', 'comment', '--comment', '100 zero prefix length ipv6'],
},
'source_destination_ipv4_no_cidr' => {
:params => {
:name => '000 source destination ipv4 no cidr',
:table => 'filter',
:provider => 'ip6tables',
:source => '1.1.1.1',
:destination => '2.2.2.2',
},
:args => ['-t', :filter, '-s', '1.1.1.1/32', '-d', '2.2.2.2/32', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv4 no cidr'],
},
'source_destination_ipv6_no_cidr' => {
:params => {
:name => '000 source destination ipv6 no cidr',
:table => 'filter',
:provider => 'ip6tables',
:source => '2001:db8:1234::',
:destination => '2001:db8:4321::',
},
:args => ['-t', :filter, '-s', '2001:db8:1234::/128', '-d', '2001:db8:4321::/128', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv6 no cidr'],
},
'source_destination_ipv6_netmask' => {
:params => {
:name => '000 source destination ipv6 netmask',
:table => 'filter',
:provider => 'ip6tables',
:source => '2001:db8:1234::/ffff:ffff:ffff:0000:0000:0000:0000:0000',
:destination => '2001:db8:4321::/ffff:ffff:ffff:0000:0000:0000:0000:0000',
},
:args => ['-t', :filter, '-s', '2001:db8:1234::/48', '-d', '2001:db8:4321::/48', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv6 netmask'],
},
'frag_ishasmorefrags' => {
:params => {
:name => "100 has more fragments",
:ishasmorefrags => true,
:provider => 'ip6tables',
:table => "filter",
},
:args => ["-t", :filter, "-p", :tcp, "-m", "frag", "--fragid", "0", "--fragmore", "-m", "comment", "--comment", "100 has more fragments"],
},
'frag_islastfrag' => {
:params => {
:name => "100 last fragment",
:islastfrag => true,
:provider => 'ip6tables',
:table => "filter",
},
:args => ["-t", :filter, "-p", :tcp, "-m", "frag", "--fragid", "0", "--fraglast", "-m", "comment", "--comment", "100 last fragment"],
},
'frag_isfirstfrags' => {
:params => {
:name => "100 first fragment",
:isfirstfrag => true,
:provider => 'ip6tables',
:table => "filter",
},
:args => ["-t", :filter, "-p", :tcp, "-m", "frag", "--fragid", "0", "--fragfirst", "-m", "comment", "--comment", "100 first fragment"],
},
'hop_limit' => {
:params => {
:name => "100 hop limit",
:hop_limit => 255,
:provider => 'ip6tables',
:table => "filter",
},
:args => ["-t", :filter, "-p", :tcp, "-m", "comment", "--comment", "100 hop limit", "-m", "hl", "--hl-eq", 255],
},
}
58 changes: 56 additions & 2 deletions spec/fixtures/iptables/conversion_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@
# This hash is for testing a line conversion to a hash of parameters
# which will be used to create a resource.
ARGS_TO_HASH = {
'dport_and_sport' => {
:line => '-A nova-compute-FORWARD -s 0.0.0.0/32 -d 255.255.255.255/32 -p udp -m udp --sport 68 --dport 67 -j ACCEPT',
:table => 'filter',
:params => {
:action => 'accept',
:chain => 'nova-compute-FORWARD',
:source => '0.0.0.0/32',
:destination => '255.255.255.255/32',
:sport => ['68'],
:dport => ['67'],
:proto => 'udp',
},
},
'long_rule_1' => {
:line => '-A INPUT -s 1.1.1.1/32 -d 1.1.1.1/32 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061,7062 -m comment --comment "000 allow foo" -j ACCEPT',
:table => 'filter',
Expand Down Expand Up @@ -89,6 +102,30 @@
:destination => '2001:db8:4321::/48',
},
},
'source_destination_negate_source' => {
:line => '-A INPUT ! -s 1.1.1.1 -d 2.2.2.2 -m comment --comment "000 negated source address"',
:table => 'filter',
:params => {
:source => '! 1.1.1.1/32',
:destination => '2.2.2.2/32',
},
},
'source_destination_negate_destination' => {
:line => '-A INPUT -s 1.1.1.1 ! -d 2.2.2.2 -m comment --comment "000 negated destination address"',
:table => 'filter',
:params => {
:source => '1.1.1.1/32',
:destination => '! 2.2.2.2/32',
},
},
'source_destination_negate_destination_alternative' => {
:line => '-A INPUT -s 1.1.1.1 -d ! 2.2.2.2 -m comment --comment "000 negated destination address alternative"',
:table => 'filter',
:params => {
:source => '1.1.1.1/32',
:destination => '! 2.2.2.2/32',
},
},
'dport_range_1' => {
:line => '-A INPUT -m multiport --dports 1:1024 -m comment --comment "000 allow foo"',
:table => 'filter',
Expand Down Expand Up @@ -170,6 +207,14 @@
:action => nil,
},
},
'ctstate_returns_sorted_values' => {
:line => '-A INPUT -m conntrack --ctstate INVALID,RELATED,ESTABLISHED',
:table => 'filter',
:params => {
:ctstate => ['ESTABLISHED', 'INVALID', 'RELATED'],
:action => nil,
},
},
'comment_string_character_validation' => {
:line => '-A INPUT -s 192.168.0.1/32 -m comment --comment "000 allow from 192.168.0.1, please"',
:table => 'filter',
Expand Down Expand Up @@ -539,15 +584,15 @@
:table => 'filter',
:dst_range => '10.0.0.1-10.0.0.10',
},
:args => ['-t', :filter, '-m', 'iprange', '--dst-range', '10.0.0.1-10.0.0.10', '-p', :tcp, '-m', 'comment', '--comment', '000 dst_range'],
:args => ['-t', :filter, '-p', :tcp, '-m', 'iprange', '--dst-range', '10.0.0.1-10.0.0.10', '-m', 'comment', '--comment', '000 dst_range'],
},
'src_range_1' => {
:params => {
:name => '000 src_range',
:table => 'filter',
:dst_range => '10.0.0.1-10.0.0.10',
},
:args => ['-t', :filter, '-m', 'iprange', '--dst-range', '10.0.0.1-10.0.0.10', '-p', :tcp, '-m', 'comment', '--comment', '000 src_range'],
:args => ['-t', :filter, '-p', :tcp, '-m', 'iprange', '--dst-range', '10.0.0.1-10.0.0.10', '-m', 'comment', '--comment', '000 src_range'],
},
'tcp_flags_1' => {
:params => {
Expand All @@ -567,6 +612,15 @@
:args => ["-t", :filter, "-p", :tcp, "-m", "comment", "--comment", "100 states_set_from_array",
"-m", "state", "--state", "ESTABLISHED,INVALID"],
},
'ctstates_set_from_array' => {
:params => {
:name => "100 ctstates_set_from_array",
:table => "filter",
:ctstate => ['ESTABLISHED', 'INVALID']
},
:args => ["-t", :filter, "-p", :tcp, "-m", "comment", "--comment", "100 ctstates_set_from_array",
"-m", "conntrack", "--ctstate", "ESTABLISHED,INVALID"],
},
'comment_string_character_validation' => {
:params => {
:name => "000 allow from 192.168.0.1, please",
Expand Down
38 changes: 38 additions & 0 deletions spec/spec_helper_acceptance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
require 'beaker-rspec'

def iptables_flush_all_tables
['filter', 'nat', 'mangle', 'raw'].each do |t|
expect(shell("/sbin/iptables -t #{t} -F").stderr).to eq("")
end
end

def ip6tables_flush_all_tables
['filter'].each do |t|
expect(shell("/sbin/ip6tables -t #{t} -F").stderr).to eq("")
end
end

hosts.each do |host|
# Install Puppet
install_package host, 'rubygems'
on host, 'gem install puppet --no-ri --no-rdoc'
on host, "mkdir -p #{host['distmoduledir']}"
end

RSpec.configure do |c|
# Project root
proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..'))

# Readable test descriptions
c.formatter = :documentation

# Configure all nodes in nodeset
c.before :suite do
# Install module and dependencies
puppet_module_install(:source => proj_root, :module_name => 'firewall')
hosts.each do |host|
shell('/bin/touch /etc/puppet/hiera.yaml')
shell('puppet module install puppetlabs-stdlib --version 3.2.0', { :acceptable_exit_codes => [0,1] })
end
end
end
49 changes: 0 additions & 49 deletions spec/spec_helper_system.rb

This file was deleted.

13 changes: 0 additions & 13 deletions spec/system/basic_spec.rb

This file was deleted.

39 changes: 0 additions & 39 deletions spec/system/class_spec.rb

This file was deleted.

29 changes: 0 additions & 29 deletions spec/system/purge_spec.rb

This file was deleted.

53 changes: 0 additions & 53 deletions spec/system/resource_cmd_spec.rb

This file was deleted.

4 changes: 2 additions & 2 deletions spec/unit/classes/firewall_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

context 'kernel => Windows' do
let(:facts) {{ :kernel => 'Windows' }}
it { expect { should include_class('firewall::linux') }.to raise_error(Puppet::Error) }
it { expect { should contain_class('firewall::linux') }.to raise_error(Puppet::Error) }
end

context 'ensure => stopped' do
Expand All @@ -20,6 +20,6 @@
context 'ensure => test' do
let(:facts) {{ :kernel => 'Linux' }}
let(:params) {{ :ensure => 'test' }}
it { expect { should include_class('firewall::linux') }.to raise_error(Puppet::Error) }
it { expect { should contain_class('firewall::linux') }.to raise_error(Puppet::Error) }
end
end
13 changes: 8 additions & 5 deletions spec/unit/facter/iptables_persistent_version_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,26 @@
}.each do |os, ver|
describe "#{os} package installed" do
before {
Facter.fact(:operatingsystem).stubs(:value).returns(os)
Facter::Util::Resolution.stubs(:exec).with(dpkg_cmd).returns(ver)
allow(Facter.fact(:operatingsystem)).to receive(:value).and_return(os)
allow(Facter::Util::Resolution).to receive(:exec).with(dpkg_cmd).
and_return(ver)
}
it { Facter.fact(:iptables_persistent_version).value.should == ver }
end
end

describe 'Ubuntu package not installed' do
before {
Facter.fact(:operatingsystem).stubs(:value).returns("Ubuntu")
Facter::Util::Resolution.stubs(:exec).with(dpkg_cmd).returns(nil)
allow(Facter.fact(:operatingsystem)).to receive(:value).and_return('Ubuntu')
allow(Facter::Util::Resolution).to receive(:exec).with(dpkg_cmd).
and_return(nil)
}
it { Facter.fact(:iptables_persistent_version).value.should be_nil }
end

describe 'CentOS not supported' do
before { Facter.fact(:operatingsystem).stubs(:value).returns("CentOS") }
before { allow(Facter.fact(:operatingsystem)).to receive(:value).
and_return("CentOS") }
it { Facter.fact(:iptables_persistent_version).value.should be_nil }
end
end
10 changes: 6 additions & 4 deletions spec/unit/facter/iptables_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@
describe "Facter::Util::Fact" do
before {
Facter.clear
Facter.fact(:kernel).stubs(:value).returns("Linux")
Facter.fact(:kernelrelease).stubs(:value).returns("2.6")
allow(Facter.fact(:kernel)).to receive(:value).and_return('Linux')
allow(Facter.fact(:kernelrelease)).to receive(:value).and_return('2.6')
}

describe 'iptables_version' do
it {
Facter::Util::Resolution.stubs(:exec).with('iptables --version').returns('iptables v1.4.7')
allow(Facter::Util::Resolution).to receive(:exec).with('iptables --version').
and_return('iptables v1.4.7')
Facter.fact(:iptables_version).value.should == '1.4.7'
}
end

describe 'ip6tables_version' do
before { Facter::Util::Resolution.stubs(:exec).with('ip6tables --version').returns('ip6tables v1.4.7') }
before { allow(Facter::Util::Resolution).to receive(:exec).
with('ip6tables --version').and_return('ip6tables v1.4.7') }
it { Facter.fact(:ip6tables_version).value.should == '1.4.7' }
end
end
18 changes: 14 additions & 4 deletions spec/unit/puppet/provider/iptables_chain_spec.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
#!/usr/bin/env rspec

require 'spec_helper'
require 'puppet'
if Puppet.version < '3.4.0'
require 'puppet/provider/confine/exists'
else
require 'puppet/confine/exists'
end

describe 'iptables chain provider detection' do
let(:exists) {
Puppet::Provider::Confine::Exists
}
if Puppet.version < '3.4.0'
let(:exists) {
Puppet::Provider::Confine::Exists
}
else
let(:exists) {
Puppet::Confine::Exists
}
end

before :each do
# Reset the default provider
Expand Down
251 changes: 246 additions & 5 deletions spec/unit/puppet/provider/iptables_spec.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
#!/usr/bin/env rspec

require 'spec_helper'
require 'puppet/provider/confine/exists'
if Puppet.version < '3.4.0'
require 'puppet/provider/confine/exists'
else
require 'puppet/confine/exists'
end

describe 'iptables provider detection' do
let(:exists) {
Puppet::Provider::Confine::Exists
}
if Puppet.version < '3.4.0'
let(:exists) {
Puppet::Provider::Confine::Exists
}
else
let(:exists) {
Puppet::Confine::Exists
}
end

before :each do
# Reset the default provider
Expand Down Expand Up @@ -44,7 +54,7 @@
}

before :each do
Puppet::Type::Firewall.stubs(:defaultprovider).returns provider
allow(Puppet::Type::Firewall).to receive(:defaultprovider).and_return provider
allow(provider).to receive(:command).with(:iptables_save).and_return "/sbin/iptables-save"

# Stub iptables version
Expand All @@ -69,6 +79,126 @@
expect(provider.instances.length).to be_zero
end

describe '#insert_order' do
let(:iptables_save_output) { [
'-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT',
'-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -m comment --comment "200 test" -j ACCEPT',
'-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT'
] }
let(:resources) do
iptables_save_output.each_with_index.collect { |l,index| provider.rule_to_hash(l, 'filter', index) }
end
let(:providers) do
resources.collect { |r| provider.new(r) }
end
it 'understands offsets for adding rules to the beginning' do
resource = Puppet::Type.type(:firewall).new({ :name => '001 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(1) # 1-indexed
end
it 'understands offsets for editing rules at the beginning' do
resource = Puppet::Type.type(:firewall).new({ :name => '100 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(1)
end
it 'understands offsets for adding rules to the middle' do
resource = Puppet::Type.type(:firewall).new({ :name => '101 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(2)
end
it 'understands offsets for editing rules at the middle' do
resource = Puppet::Type.type(:firewall).new({ :name => '200 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(2)
end
it 'understands offsets for adding rules to the end' do
resource = Puppet::Type.type(:firewall).new({ :name => '301 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(4)
end
it 'understands offsets for editing rules at the end' do
resource = Puppet::Type.type(:firewall).new({ :name => '300 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(3)
end

context 'with unname rules between' do
let(:iptables_save_output) { [
'-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT',
'-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 150 -m comment --comment "150 test" -j ACCEPT',
'-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -j ACCEPT',
'-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 250 -j ACCEPT',
'-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT',
'-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 350 -m comment --comment "350 test" -j ACCEPT',
] }
it 'understands offsets for adding rules before unnamed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '001 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(1)
end
it 'understands offsets for editing rules before unnamed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '100 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(1)
end
it 'understands offsets for adding rules between managed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '120 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(2)
end
it 'understands offsets for adding rules between unnamed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '151 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(3)
end
it 'understands offsets for adding rules after unnamed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '351 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(7)
end
end

context 'with unname rules before and after' do
let(:iptables_save_output) { [
'-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 050 -j ACCEPT',
'-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 090 -j ACCEPT',
'-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 100 -m comment --comment "100 test" -j ACCEPT',
'-A INPUT -s 8.0.0.2/32 -p tcp -m multiport --ports 150 -m comment --comment "150 test" -j ACCEPT',
'-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 200 -j ACCEPT',
'-A INPUT -s 8.0.0.3/32 -p tcp -m multiport --ports 250 -j ACCEPT',
'-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 300 -m comment --comment "300 test" -j ACCEPT',
'-A INPUT -s 8.0.0.4/32 -p tcp -m multiport --ports 350 -m comment --comment "350 test" -j ACCEPT',
'-A INPUT -s 8.0.0.5/32 -p tcp -m multiport --ports 400 -j ACCEPT',
'-A INPUT -s 8.0.0.5/32 -p tcp -m multiport --ports 450 -j ACCEPT',
] }
it 'understands offsets for adding rules before unnamed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '001 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(1)
end
it 'understands offsets for editing rules before unnamed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '100 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(3)
end
it 'understands offsets for adding rules between managed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '120 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(4)
end
it 'understands offsets for adding rules between unnamed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '151 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(5)
end
it 'understands offsets for adding rules after unnamed rules' do
resource = Puppet::Type.type(:firewall).new({ :name => '351 test', })
allow(resource.provider.class).to receive(:instances).and_return(providers)
expect(resource.provider.insert_order).to eq(9)
end
end
end

# Load in ruby hash for test fixtures.
load 'spec/fixtures/iptables/conversion_hash.rb'

Expand Down Expand Up @@ -123,6 +253,37 @@
it 'rule name contains a MD5 sum of the line' do
expect(resource[:name]).to eq("9000 #{Digest::MD5.hexdigest(resource[:line])}")
end

it 'parsed the rule arguments correctly' do
expect(resource[:chain]).to eq('INPUT')
expect(resource[:source]).to eq('1.1.1.1/32')
expect(resource[:destination]).to eq('1.1.1.1/32')
expect(resource[:proto]).to eq('tcp')
expect(resource[:dport]).to eq(['7061', '7062'])
expect(resource[:sport]).to eq(['7061', '7062'])
expect(resource[:action]).to eq('accept')
end
end

describe 'when converting existing rules generates by system-config-firewall-tui to resources' do
let(:sample_rule) {
# as generated by iptables-save from rules created with system-config-firewall-tui
'-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT'
}
let(:resource) { provider.rule_to_hash(sample_rule, 'filter', 0) }
let(:instance) { provider.new(resource) }

it 'rule name contains a MD5 sum of the line' do
expect(resource[:name]).to eq("9000 #{Digest::MD5.hexdigest(resource[:line])}")
end

it 'parse arguments' do
expect(resource[:chain]).to eq('INPUT')
expect(resource[:proto]).to eq('tcp')
expect(resource[:dport]).to eq(['22'])
expect(resource[:state]).to eq(['NEW'])
expect(resource[:action]).to eq('accept')
end
end

describe 'when creating resources' do
Expand All @@ -139,6 +300,10 @@
it 'update_args should be an array' do
expect(instance.update_args.class).to eq(Array)
end

it 'fails when modifying the chain' do
expect { instance.chain = "OUTPUT" }.to raise_error(/is not supported/)
end
end

describe 'when deleting resources' do
Expand All @@ -162,3 +327,79 @@
end
end
end

describe 'ip6tables provider' do
let(:provider6) { Puppet::Type.type(:firewall).provider(:ip6tables) }
let(:resource) {
Puppet::Type.type(:firewall).new({
:name => '000 test foo',
:action => 'accept',
:provider => "ip6tables",
})
}

before :each do
allow(Puppet::Type::Firewall).to receive(:ip6tables).and_return provider6
allow(provider6).to receive(:command).with(:ip6tables_save).and_return "/sbin/ip6tables-save"

# Stub iptables version
allow(Facter.fact(:ip6tables_version)).to receive(:value).and_return '1.4.7'

allow(Puppet::Util::Execution).to receive(:execute).and_return ''
allow(Puppet::Util).to receive(:which).with("ip6tables-save").
and_return "/sbin/ip6tables-save"
end

it 'should be able to get a list of existing rules' do
provider6.instances.each do |rule|
rule.should be_instance_of(provider6)
rule.properties[:provider6].to_s.should == provider6.name.to_s
end
end

it 'should ignore lines with fatal errors' do
allow(Puppet::Util::Execution).to receive(:execute).with(['/sbin/ip6tables-save']).
and_return("FATAL: Could not load /lib/modules/2.6.18-028stab095.1/modules.dep: No such file or directory")
provider6.instances.length.should == 0
end

# Load in ruby hash for test fixtures.
load 'spec/fixtures/ip6tables/conversion_hash.rb'

describe 'when converting rules to resources' do
ARGS_TO_HASH6.each do |test_name,data|
describe "for test data '#{test_name}'" do
let(:resource) { provider6.rule_to_hash(data[:line], data[:table], 0) }

# If this option is enabled, make sure the parameters exactly match
if data[:compare_all] then
it "the parameter hash keys should be the same as returned by rules_to_hash" do
resource.keys.should =~ data[:params].keys
end
end

# Iterate across each parameter, creating an example for comparison
data[:params].each do |param_name, param_value|
it "the parameter '#{param_name.to_s}' should match #{param_value.inspect}" do
resource[param_name].should == data[:params][param_name]
end
end
end
end
end

describe 'when working out general_args' do
HASH_TO_ARGS6.each do |test_name,data|
describe "for test data '#{test_name}'" do
let(:resource) { Puppet::Type.type(:firewall).new(data[:params]) }
let(:provider6) { Puppet::Type.type(:firewall).provider(:ip6tables) }
let(:instance) { provider6.new(resource) }

it 'general_args should be valid' do
instance.general_args.flatten.should == data[:args]
end
end
end
end
end

Loading