292 changes: 292 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
## puppetlabs-firewall module

### Overview

This is the puppet-firewall module. Here we are providing a module which can be used to configure various firewalls

### Disclaimer

Warning! While this software is written in the best interest of quality it has not been formally tested by our QA teams. Use at your own risk, but feel free to enjoy and perhaps improve it while you do.

Please see the included Apache Software License for more legal details regarding warranty.

### Installation

From github, download the module into your modulepath on your Puppetmaster. If you are not sure where your module path is try this command:

puppet --configprint modulepath

Depending on the version of Puppet, you may need to restart the puppetmasterd (or Apache) process before this module will work.

This module uses both Ruby based providers so your Puppet configuration (ie. puppet.conf) must include the following items:

[agent]
pluginsync = true

The module will not operate normally without these features enabled.

### Quickstart

Once the module is in the correct modulepath, you should be able to create some
firewall rules like the below examples. Remember, that rules are lexically
ordered by the resource title at this point.

Basic accept ICMP request example:

firewall { "000 accept all icmp requests":
proto => "icmp",
action => "accept",
}

Deny all:

firewall { "999 drop all other requests":
action => "drop",
}

Source NAT example (perfect for a virtualization host):

firewall { '100 snat for network foo2':
chain => 'POSTROUTING',
jump => 'MASQUERADE',
proto => 'all',
outiface => "eth0",
source => ['10.1.2.0/24'],
table => 'nat',
}

You can make firewall rules persistent with the following iptables example:

exec { "persist-firewall":
command => $operatingsystem ? {
"debian" => "/sbin/iptables-save > /etc/iptables/rules.v4",
/(RedHat|CentOS)/ => "/sbin/iptables-save > /etc/sysconfig/iptables",
}
refreshonly => true,
}
Firewall {
notify => Exec["persist-firewall"]
}

If you wish to ensure any reject rules are executed last, try using stages.
The following example shows the creation of a class which is where your
last rules should run, this however should belong in a puppet module.

class my_fw::drop {
iptables { "999 drop all":
action => "drop"
}
}

stage { pre: before => Stage[main] }
stage { post: require => Stage[main] }

class { "my_fw::drop": stage => "post" }

By placing the 'my_fw::drop' class in the post stage it will always be inserted
last thereby avoiding locking you out before the accept rules are inserted.

### Supported firewalls

Currently we support:

* Iptables

But plans are to support lots of other firewall implementations:

* Linux IPv6 (ip6tables)
* FreeBSD (ipf)
* Mac OS X (ipfw)
* OpenBSD (pf)
* Cisco (ASA and basic access lists)

If you have knowledge in these rules and wish to contribute to this project
feel free to submit patches (after signing a Puppetlabs CLA :-).

### Generic Properties

#### ensure

Creates rule when present, removes it when absent.

#### name

* namevar

Name of firewall rule. This at the moment also is used for ordering, so its
common practice to prefix all rules with numbers to force ordering. For example:

name => "000 accept local traffic"

This will occur very early.

#### action

This is the action to perform on a match. Can be one of:

* accept - the packet is accepted
* reject - the packet is rejected with a suitable ICMP response
* drop - the packet is dropped

If you specify no value it will simply match the rule but perform no
action unless you provide a provider specific parameter (such as 'jump').

#### proto

Protocol to filter. By default this is 'tcp'.

#### source

An array of source addresses. For example:

source => ['192.168.2.0/24', '10.2.3.0/24']

#### destination

An array of destination addresses to match. For example:

destination => ['192.168.2.0/24', '10.2.3.0/24']

#### sport

For protocols that support ports, this is a list of source ports to filter on.

#### dport

For protocols that support ports, this is a list of destination ports to filter on.

### Iptables Properties

#### chain

Name of the chain to use. Can be one of the built-ins:

* INPUT
* FORWARD
* OUTPUT
* PREROUTING
* POSTROUTING

Or you can provide a user-based chain.

The default value is 'INPUT'.

#### table

Table to use. Can be one of:

* nat
* mangle
* filter
* raw
* rawpost

By default the setting is 'filter'.

#### jump

Action to perform when filter is matched for iptables. Can be one of:

* QUEUE
* RETURN
* DNAT
* SNAT
* LOG
* MASQUERADE
* REDIRECT

But any valid chain name is allowed.

For the values ACCEPT, DROP and REJECT you must use the generic
'action' parameter. This is to enfore the use of generic parameters where
possible for maximum cross-platform modelling.

If you set both 'accept' and 'jump' parameters, you will get an error as
only one of the options should be set.

### Interface Matching Properties

#### iniface

Input interface to filter on.

#### outiface

Output interface to filter on.

### NAT Properties

#### tosource

When using jump => "SNAT" you can specify the new source address using this
parameter.

#### todestination

When using jump => "DNAT" you can specify the new destination address using
this paramter.

#### toports

Specifies a range of ports to use for masquerade.

### Reject Properties

#### reject

When combined with jump => "REJECT" you can specify a different icmp response
to be sent back to the packet sender.

### Logging Properties

#### log_level

When combined with jump => "LOG" specifies the log level to log to.

#### log_prefix

When combined with jump => "LOG" specifies the log prefix to use when logging.

### ICMP Matching Properties

#### icmp

Specifies the type of ICMP to match.

### State Matching Properties

#### state

When matching using stateful inspection you can match on different states such
as:

* INVALID
* ESTABLISHED
* NEW
* RELATED

### Rate Limiting Properties

#### limit

A rate to limit matched packets in the form of:

rate/[/second/|/minute|/hour|/day]

#### burst

Maximum initial packets to match before limit checks (above) apply.

### Testing

Make sure you have:

rake

Install the necessary gems:

gem install rspec

And run the tests from the root of the source code:

rake test
19 changes: 0 additions & 19 deletions README.md

This file was deleted.

22 changes: 22 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require 'rake'
require 'rspec/core/rake_task'

task :default do
sh %{rake -T}
end

# Aliases for spec. The (s) versions are used by rvm specs/tests.
task :test => [:spec]
task :tests => [:spec]
task :specs => [:spec]

desc 'Run all RSpec tests'
RSpec::Core::RakeTask.new(:spec) do |t|
t.rspec_opts = ['--color']
end

desc 'Generate code coverage'
RSpec::Core::RakeTask.new(:coverage) do |t|
t.rcov = true
t.rcov_opts = ['--exclude', 'spec']
end
104 changes: 104 additions & 0 deletions examples/ip6tables/test.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
firewall { '000 allow foo':
dport => [7061, 7062],
jump => 'ACCEPT',
proto => 'tcp',
provider => 'ip6tables'
}

firewall { '001 allow boo':
jump => 'ACCEPT',
iniface => 'eth0',
sport => 123,
dport => 123,
proto => 'tcp',
destination => '::1/128',
provider => 'ip6tables'
}

firewall { '002 foo':
dport => 1233,
proto => 'tcp',
jump => 'DROP',
provider => 'ip6tables'
}

firewall { '005 INPUT disregard DHCP':
dport => ['bootpc', 'bootps'],
jump => 'DROP',
proto => 'udp',
provider => 'ip6tables'
}

firewall { '006 INPUT disregard netbios':
dport => ['netbios-ns', 'netbios-dgm', 'netbios-ssn'],
jump => 'DROP',
proto => 'udp',
provider => 'ip6tables'
}

firewall { '006 Disregard CIFS':
dport => 'microsoft-ds',
jump => 'DROP',
proto => 'tcp',
provider => 'ip6tables'
}

firewall { '010 icmp':
proto => 'ipv6-icmp',
icmp => 'echo-reply',
jump => 'ACCEPT',
provider => 'ip6tables'
}

firewall { '010 INPUT allow loopback':
iniface => 'lo',
chain => 'INPUT',
jump => 'ACCEPT',
provider => 'ip6tables'
}

firewall { '050 INPUT drop invalid':
state => 'INVALID',
jump => 'DROP',
provider => 'ip6tables'
}

firewall { '051 INPUT allow related and established':
state => ['RELATED', 'ESTABLISHED'],
jump => 'ACCEPT',
provider => 'ip6tables'
}

firewall { '053 INPUT allow ICMP':
icmp => '8',
proto => 'ipv6-icmp',
jump => 'ACCEPT',
provider => 'ip6tables'
}

firewall { '055 INPUT allow DNS':
sport => 'domain',
proto => 'udp',
jump => 'ACCEPT',
provider => 'ip6tables'
}

firewall { '999 FORWARD drop':
chain => 'FORWARD',
jump => 'DROP',
provider => 'ip6tables'
}

firewall { '001 OUTPUT allow loopback':
chain => 'OUTPUT',
outiface => 'lo',
jump => 'ACCEPT',
provider => 'ip6tables'
}

firewall { '100 OUTPUT drop invalid':
chain => 'OUTPUT',
state => 'INVALID',
jump => 'DROP',
provider => 'ip6tables'
}
35 changes: 35 additions & 0 deletions examples/iptables/readme.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
firewall { '000 allow packets with valid state':
state => ['RELATED', 'ESTABLISHED'],
jump => 'ACCEPT',
}
firewall { '001 allow icmp':
proto => 'icmp',
jump => 'ACCEPT',
}
firewall { '002 allow all to lo interface':
iniface => 'lo',
jump => 'ACCEPT',
}
firewall { '100 allow http':
proto => 'tcp',
dport => '80',
jump => 'ACCEPT',
}
firewall { '100 allow ssh':
proto => 'tcp',
dport => '22',
jump => 'ACCEPT',
}
firewall { '100 allow mysql from internal':
proto => 'tcp',
dport => '3036',
source => '10.5.5.0/24',
jump => 'ACCEPT',
}
firewall { '999 drop everything else':
jump => 'DROP',
}

resources { 'firewall':
purge => true,
}
3 changes: 3 additions & 0 deletions examples/iptables/run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/bin/bash

puppet apply --debug --libdir ../../lib readme.pp
109 changes: 109 additions & 0 deletions examples/iptables/test.pp
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
firewall { '000 allow foo':
dport => [7061, 7062],
action => accept,
proto => "tcp",
}

firewall { '001 allow boo':
action => accept,
iniface => "eth0",
sport => "123",
dport => "123",
proto => "tcp",
destination => "1.1.1.0/24",
source => "2.2.2.0/24",
}

firewall { '100 snat for network foo2':
chain => 'POSTROUTING',
jump => 'MASQUERADE',
proto => 'all',
outiface => 'eth0',
source => '10.1.2.0/24',
table => 'nat'
}

firewall { '999 bar':
action => accept,
dport => "1233",
proto => "tcp",
}

firewall { '002 foo':
action => drop,
dport => "1233",
proto => "tcp",
}

firewall { '010 icmp':
action => accept,
proto => "icmp",
icmp => "echo-reply",
}

firewall { '010 INPUT allow loopback':
action => accept,
iniface => 'lo',
chain => 'INPUT',
}

firewall { '005 INPUT disregard DHCP':
action => drop,
dport => ['bootpc', 'bootps'],
proto => 'udp'
}

firewall { '006 INPUT disregard netbios':
action => drop,
proto => 'udp',
dport => ['netbios-ns', 'netbios-dgm', 'netbios-ssn'],
}

firewall { '006 Disregard CIFS':
action => drop,
dport => 'microsoft-ds',
proto => 'tcp'
}

firewall { '050 INPUT drop invalid':
action => drop,
state => 'INVALID',
}

firewall { '051 INPUT allow related and established':
action => accept,
state => ['RELATED', 'ESTABLISHED'],
}

firewall { '053 INPUT allow ICMP':
action => accept,
icmp => '8',
proto => 'icmp',
}

firewall { '055 INPUT allow DNS':
action => accept,
proto => 'udp',
sport => 'domain'
}

firewall { '999 FORWARD drop':
action => drop,
chain => 'FORWARD',
}

firewall { '001 OUTPUT allow loopback':
action => accept,
chain => 'OUTPUT',
outiface => 'lo',
}

firewall { '100 OUTPUT drop invalid':
action => drop,
chain => 'OUTPUT',
state => 'INVALID',
}

resources { 'firewall':
purge => true
}
Empty file added lib/facter/iptables.rb
Empty file.
60 changes: 60 additions & 0 deletions lib/puppet/provider/firewall.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
class Puppet::Provider::Firewall < Puppet::Provider

# Prefetch our rule list. This is ran once every time before any other
# action (besides initialization of each object).
def self.prefetch(resources)
debug("[prefetch(resources)]")
instances.each do |prov|
if resource = resources[prov.name] || resources[prov.name.downcase]
resource.provider = prov
end
end
end

# Look up the current status. This allows us to conventiently look up
# existing status with properties[:foo].
def properties
if @property_hash.empty?
@property_hash = query || {:ensure => :absent}
@property_hash[:ensure] = :absent if @property_hash.empty?
end
@property_hash.dup
end

# Pull the current state of the list from the full list. We're
# getting some double entendre here....
def query
self.class.instances.each do |instance|
if instance.name == self.name or instance.name.downcase == self.name
return instance.properties
end
end
nil
end

# Executed if method is missing. In this case we are going to catch
# unqualified property methods for dynamic property setting and getting.
def method_missing(meth, *args, &block)
dynamic_methods = self.class.instance_variable_get('@resource_map').keys
dynamic_methods << :chain
dynamic_methods << :table
dynamic_methods << :action

if dynamic_methods.include?(meth.to_sym) then
if @property_hash[meth.to_sym] then
return @property_hash[meth.to_sym]
else
return nil
end
elsif dynamic_methods.include?(meth.to_s.chomp("=").to_sym) then
debug("Args: #{args}")
@property_hash[:needs_change] = true
return true
end

debug("Dynamic methods: #{dynamic_methods.join(' ')}")
debug("Method missing: #{meth}. Calling super.")

super
end
end
45 changes: 45 additions & 0 deletions lib/puppet/provider/firewall/ip6tables.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Puppet::Type.type(:firewall).provide :ip6tables, :parent => :iptables, :source => :iptables do
@doc = "Ip6tables type provider"

has_feature :iptables
has_feature :rate_limiting
has_feature :snat
has_feature :dnat
has_feature :interface_match
has_feature :icmp_match
has_feature :state_match
has_feature :reject_type
has_feature :log_level
has_feature :log_prefix

commands :iptables => '/sbin/ip6tables'
commands :iptables_save => '/sbin/ip6tables-save'

@resource_map = {
:burst => "--limit-burst",
:destination => "-d",
:dport => "-m multiport --dports",
:icmp => "-m icmp6 --icmpv6-type",
:iniface => "-i",
:jump => "-j",
:limit => "--limit",
:log_level => "--log-level",
:log_prefix => "--log-prefix",
:name => "-m comment --comment",
:outiface => "-o",
:proto => "-p",
:reject => "--reject-with",
:source => "-s",
:state => "-m state --state",
:sport => "-m multiport --sports",
:table => "-t",
:todest => "--to-destination",
:toports => "--to-ports",
:tosource => "--to-source",
}

@resource_list = [:table, :source, :destination, :iniface, :outiface,
:proto, :sport, :dport, :name, :state, :icmp, :limit, :burst, :jump,
:todest, :tosource, :toports, :log_level, :log_prefix, :reject]

end
236 changes: 236 additions & 0 deletions lib/puppet/provider/firewall/iptables.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
require 'puppet/provider/firewall'
require 'digest/md5'

Puppet::Type.type(:firewall).provide :iptables, :parent => Puppet::Provider::Firewall do
include Puppet::Util::Firewall

@doc = "Iptables type provider"

has_feature :iptables
has_feature :rate_limiting
has_feature :snat
has_feature :dnat
has_feature :interface_match
has_feature :icmp_match
has_feature :state_match
has_feature :reject_type
has_feature :log_level
has_feature :log_prefix

commands :iptables => '/sbin/iptables'
commands :iptables_save => '/sbin/iptables-save'

defaultfor :kernel => :linux

@resource_map = {
:burst => "--limit-burst",
:destination => "-d",
:dport => "-m multiport --dports",
:icmp => "-m icmp --icmp-type",
:iniface => "-i",
:jump => "-j",
:limit => "--limit",
:log_level => "--log-level",
:log_prefix => "--log-prefix",
:name => "-m comment --comment",
:outiface => "-o",
:proto => "-p",
:reject => "--reject-with",
:source => "-s",
:state => "-m state --state",
:sport => "-m multiport --sports",
:table => "-t",
:todest => "--to-destination",
:toports => "--to-ports",
:tosource => "--to-source",
}

@resource_list = [:table, :source, :destination, :iniface, :outiface,
:proto, :sport, :dport, :name, :state, :icmp, :limit, :burst, :jump,
:todest, :tosource, :toports, :log_level, :log_prefix, :reject]

def insert
debug 'Inserting rule %s' % resource[:name]
iptables insert_args
end

def update
debug 'Updating rule %s' % resource[:name]
iptables update_args
end

def delete
debug 'Deleting rule %s' % resource[:name]
iptables delete_args
end

def exists?
properties[:ensure] != :absent
end

# Flush the property hash once done.
def flush
debug("[flush]")
if @property_hash.delete(:needs_change)
notice("Properties changed - updating rule")
update
end
@property_hash.clear
end

def self.instances
debug "[instances]"
table = nil
rules = []
counter = 1

# String#lines would be nice, but we need to support Ruby 1.8.5
iptables_save.split("\n").each do |line|
unless line =~ /^\#\s+|^\:\S+|^COMMIT/
if line =~ /^\*/
table = line.sub(/\*/, "")
else
if hash = rule_to_hash(line, table, counter)
rules << new(hash)
counter += 1
end
end
end
end
rules
end

def self.rule_to_hash(line, table, counter)
hash = {}
keys = []
values = line.dup

@resource_list.reverse.each do |k|
if values.slice!(/\s#{@resource_map[k]}/)
keys << k
end
end

# Manually remove chain
values.slice!('-A')
keys << :chain

keys.zip(values.scan(/"[^"]*"|\S+/).reverse) { |f, v| hash[f] = v.gsub(/"/, '') }

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

# This forces all existing, commentless rules to be moved to the bottom of the stack.
# Puppet-firewall requires that all rules have comments (resource names) and will fail if
# a rule in iptables does not have a comment. We get around this by appending a high level
if ! hash[:name]
hash[:name] = "9999 #{Digest::MD5.hexdigest(line)}"
end

hash[:line] = line
hash[:provider] = self.name.to_s
hash[:table] = table
hash[:ensure] = :present

# Munge some vars here ...

# Proto should equal 'all' if undefined
hash[:proto] = "all" if !hash.include?(:proto)

# If the jump parameter is set to one of: ACCEPT, REJECT or DROP then
# we should set the action parameter instead.
if ['ACCEPT','REJECT','DROP'].include?(hash[:jump]) then
hash[:action] = hash[:jump].downcase
hash.delete(:jump)
end

hash
end

def insert_args
args = []
args << ["-I", resource[:chain], insert_order]
args << general_args
args
end

def update_args
args = []
args << ["-R", resource[:chain], insert_order]
args << general_args
args
end

def delete_args
count = []
line = properties[:line].gsub(/\-A/, '-D').split

# Grab all comment indices
line.each do |v|
if v =~ /"/
count << line.index(v)
end
end

if ! count.empty?
# Remove quotes and set first comment index to full string
line[count.first] = line[count.first..count.last].join(' ').gsub(/"/, '')

# Make all remaining comment indices nil
((count.first + 1)..count.last).each do |i|
line[i] = nil
end
end

# Return array without nils
line.compact
end

def general_args
debug "Current resource: %s" % resource.class

args = []
resource_list = self.class.instance_variable_get('@resource_list')
resource_map = self.class.instance_variable_get('@resource_map')

resource_list.each do |res|
resource_value = nil
if (resource[res]) then
resource_value = resource[res]
elsif res == :jump and resource[:action] then
# In this case, we are substituting jump for action
resource_value = resource[:action].to_s.upcase
else
next
end

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

if resource_value.is_a?(Array)
args << resource_value.join(',')
else
args << resource_value
end
end

args
end

def insert_order
debug("[insert_order]")
rules = []

# Find list of current rules based on chain
self.class.instances.each do |rule|
rules << rule.name if rule.chain == resource[:chain].to_s
end

# No rules at all? Just bail now.
return 1 if rules.empty?

my_rule = resource[:name].to_s
rules << my_rule
rules.sort.index(my_rule) + 1
end
end
310 changes: 310 additions & 0 deletions lib/puppet/type/firewall.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
# Puppet Firewall type
require 'puppet/util/firewall'

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

@doc = "This type provides the capability to manage firewall rules within
puppet."

feature :rate_limiting, "Rate limiting features."
feature :snat, "Source NATing"
feature :dnat, "Destination NATing"
feature :interface_match, "Interface matching"
feature :icmp_match, "Matching ICMP types"
feature :state_match, "Matching stateful firewall states"
feature :reject_type, "The ability to control reject messages"
feature :log_level, "The ability to control the log level"
feature :log_prefix, "The ability to add prefixes to log messages"

# provider specific features
feature :iptables, "The provider provides iptables features."

ensurable do
desc "Manage the state of this rule."

newvalue(:present) do
provider.insert
end

newvalue(:absent) do
provider.delete
end

defaultto :present
end

newparam(:name) do
desc "The canonical name of the rule."
isnamevar

# Keep rule names simple - they must start with a number
newvalues(/^\d+[a-zA-Z0-9\s\-_]+$/)
end

newproperty(:action) do
desc "Action to perform on this rule."
newvalues(:accept, :reject, :drop)
end

# Generic matching properties
newproperty(:source) do
desc "The source IP address to match."
end

newproperty(:destination) do
desc "The destination IP address to match."
end

newproperty(:sport, :array_matching => :all) do
desc "The source port to match for this filter (if the protocol supports
ports). Will accept a single element or an array."

munge do |value|
@resource.string_to_port(value)
end

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

newproperty(:dport, :array_matching => :all) do
desc "The destination port to match for this filter (if the protocol
supports ports). Will accept a single element or an array."

munge do |value|
@resource.string_to_port(value)
end

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

newproperty(:proto) do
desc "The specific protocol to match for this rule."
newvalues(:tcp, :udp, :icmp, :"ipv6-icmp", :esp, :ah, :vrrp, :igmp, :all)
defaultto "tcp"
end

# Iptables specific
newproperty(:chain, :required_features => :iptables) do
desc "The value for the iptables -A parameter. Normal values are: 'INPUT',
'FORWARD', 'OUTPUT', 'PREROUTING', 'POSTROUTING' but you can also
specify a user created chain."

defaultto "INPUT"
newvalue(/^[a-zA-Z0-9\-_]+$/)
end

newproperty(:table, :required_features => :iptables) do
desc "The value for the iptables -t parameter."
newvalues(:nat, :mangle, :filter, :raw, :rawpost)
defaultto "filter"
end

newproperty(:jump, :required_features => :iptables) do
desc <<EOS
The value for the iptables --jump parameter. Normal values are:
* QUEUE
* RETURN
* DNAT
* SNAT
* LOG
* MASQUERADE
* REDIRECT.
But any valid chain name is allowed.
For the values ACCEPT, DROP and REJECT you must use the generic
'action' parameter. This is to enfore the use of generic parameters where
possible for maximum cross-platform modelling.
If you set both 'accept' and 'jump' parameters, the jump parameter will take
precedence.
EOS
validate do |value|
raise ArgumentError, "Jump destination must consist of alphanumeric characters, an underscore or a hyphen." unless value =~ /^[a-zA-Z0-9\-_]+$/
raise ArgumentError, "Jump destination should not be one of ACCEPT, REJECT or DENY. Use the action property instead." if ["accept","reject","drop"].include?(value.downcase)
end
end

# Interface specific matching properties
newproperty(:iniface, :required_features => :interface_match) do
desc "Match input interface."
newvalues(/^[a-zA-Z0-9\-_]+$/)
end

newproperty(:outiface, :required_features => :interface_match) do
desc "Match ouput interface."
newvalues(/^[a-zA-Z0-9\-_]+$/)
end

# NAT specific properties
newproperty(:tosource, :required_features => :snat) do
desc "For SNAT this is the IP address that will replace the source IP
address."
end

newproperty(:todest, :required_features => :dnat) do
desc "For DNAT this is the IP address that will replace the destination IP
address."
end

newproperty(:toports, :required_features => :dnat) do
desc "For DNAT this is the port that will replace the destination port."
end

# Reject ICMP type
newproperty(:reject, :required_features => :reject_type) do
desc "The ICMP response to reject a packet with."
end

# Logging properties
newproperty(:log_level, :required_features => :log_level) do
desc "The syslog level to log to."
end

newproperty(:log_prefix, :required_features => :log_prefix) do
desc "The syslog prefix."
end

# ICMP matching property
newproperty(:icmp, :required_features => :icmp_match) do
desc "When matching ICMP packets, this is the type of ICMP packet to match."

munge do |value|
if value.kind_of?(String)
value = @resource.icmp_name_to_number(value)
else
value
end

if value == nil && value != ""
self.fail("cannot work out icmp type")
end
value
end
end

newproperty(:state, :array_matching => :all, :required_features => :state_match) do
desc "Matches a packet based on its state in the firewall stateful inspection
table."

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

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

# Rate limiting properties
newproperty(:limit, :required_features => :rate_limiting) do
desc "Rate limiting value. Example values are: '50/sec', '40/min',
'30/hour', '10/day'."
end

newproperty(:burst, :required_features => :rate_limiting) do
desc "Rate limiting burst value (per second)."
newvalue(/^\d+$/)
end

newparam(:line) do
desc 'Read-only property for caching the rule line'
end

validate do
debug("[validate]")

# TODO: this is put here to skip validation if ensure is not set. This
# is because there is a revalidation stage called later where the values
# are not set correctly. I tried tracing it - but have put in this
# workaround instead to skip. Must get to the bottom of this.
if ! value(:ensure)
return
end

# First we make sure the chains and tables are valid combinations
if value(:table).to_s == "filter" && value(:chain) =~ /PREROUTING|POSTROUTING/
self.fail "PREROUTING and POSTROUTING cannot be used in table 'filter'"
end

if value(:table).to_s == "nat" && value(:chain) =~ /INPUT|FORWARD/
self.fail "INPUT and FORWARD cannot be used in table 'nat'"
end

if value(:table).to_s == "raw" && value(:chain) =~ /INPUT|FORWARD|POSTROUTING/
self.fail "INPUT, FORWARD and POSTROUTING cannot be used in table raw"
end

# Now we analyse the individual properties to make sure they apply to
# the correct combinations.
if value(:iniface)
unless value(:chain).to_s =~ /INPUT|FORWARD|PREROUTING/
self.fail "Parameter iniface only applies to chains " \
"INPUT,FORWARD,PREROUTING"
end
end

if value(:outiface)
unless value(:chain).to_s =~ /OUTPUT|FORWARD|POSTROUTING/
self.fail "Parameter outiface only applies to chains " \
"OUTPUT,FORWARD,POSTROUTING"
end
end

if value(:dport)
unless value(:proto).to_s =~ /tcp|udp|sctp/
self.fail "[%s] Parameter dport only applies to sctp, tcp and udp " \
"protocols. Current protocol is [%s] and dport is [%s]" %
[value(:name), should(:proto), should(:dport)]
end
end

if value(:jump).to_s == "DNAT"
unless value(:table).to_s =~ /nat/
self.fail "Parameter jump => DNAT only applies to table => nat"
end

unless value(:todest)
self.fail "Parameter jump => DNAT must have todest parameter"
end
end

if value(:jump).to_s == "SNAT"
unless value(:table).to_s =~ /nat/
self.fail "Parameter jump => SNAT only applies to table => nat"
end

unless value(:tosource)
self.fail "Parameter jump => DNAT must have tosource parameter"
end
end

if value(:jump).to_s == "REDIRECT"
unless value(:toports)
self.fail "Parameter jump => REDIRECT missing mandatory toports " \
"parameter"
end
end

if value(:jump).to_s == "MASQUERADE"
unless value(:table).to_s =~ /nat/
self.fail "Parameter jump => MASQUERADE only applies to table => nat"
end
end

if value(:burst) && ! value(:limit)
self.fail "burst makes no sense without limit"
end

if value(:action) && value(:jump)
self.fail "Only one of the parameters 'action' and 'jump' can be set"
end
end
end
59 changes: 59 additions & 0 deletions lib/puppet/util/firewall.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Puppet Firewall Module
#
# Copyright (C) 2011 Bob.sh Limited
# Copyright (C) 2008 Camptocamp Association
# Copyright (C) 2007 Dmitri Priimak
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

require 'socket'
require 'puppet/util/ipcidr'

module Puppet::Util::Firewall
# Translate the symbolic names for icmp packet types to integers
def icmp_name_to_number(value_icmp)
if value_icmp =~ /\d{1,2}$/
value_icmp
else
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
end
end

def string_to_port(value)
if value.kind_of?(Array)
ports = []
value.each do |port|
ports << Socket.getservbyname(port) unless port.kind_of?(Integer)
end
ports
else
Socket.getservbyname(value)
end
end
end
49 changes: 49 additions & 0 deletions lib/puppet/util/ipcidr.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Puppet Firewall Module
#
# Copyright (C) 2011 Bob.sh Limited
# Copyright (C) 2008 Camptocamp Association
# Copyright (C) 2007 Dmitri Priimak
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

require 'ipaddr'

module Puppet
module Util
class IPCidr < IPAddr

def netmask
_to_string(@mask_addr)
end

def prefixlen
m = case @family
when Socket::AF_INET
IN4MASK
when Socket::AF_INET6
IN6MASK
else
raise "unsupported address family"
end
return $1.length if /\A(1*)(0*)\z/ =~ (@mask_addr & m).to_s(2)
raise "bad addr_mask format"
end

def cidr
cidr = sprintf("%s/%s", self.to_s, self.prefixlen)
cidr
end
end
end
end
102 changes: 102 additions & 0 deletions spec/fixtures/iptables/conversion_hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 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_HASH = {
'long_rule_1' => {
:line => '-A INPUT -s 1.1.1.1 -d 1.1.1.1 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061,7062 -m comment --comment "000 allow foo" -j ACCEPT',
:table => 'filter',
:compare_all => true,
:params => {
:action => "accept",
:chain => "INPUT",
:destination => "1.1.1.1",
:dport => ["7061","7062"],
:ensure => :present,
:line => '-A INPUT -s 1.1.1.1 -d 1.1.1.1 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061,7062 -m comment --comment "000 allow foo" -j ACCEPT',
:name => "000 allow foo",
:proto => "tcp",
:provider => "iptables",
:source => "1.1.1.1",
:sport => ["7061","7062"],
:table => "filter",
},
},
'action_drop_1' => {
:line => '-A INPUT -m comment --comment "000 allow foo" -j DROP',
:table => 'filter',
:params => {
:jump => nil,
:action => "drop",
},
},
'action_reject_1' => {
:line => '-A INPUT -m comment --comment "000 allow foo" -j REJECT',
:table => 'filter',
:params => {
:jump => nil,
:action => "reject",
},
},
'action_nil_1' => {
:line => '-A INPUT -m comment --comment "000 allow foo"',
:table => 'filter',
:params => {
:jump => nil,
:action => nil,
},
},
'jump_custom_chain_1' => {
:line => '-A INPUT -m comment --comment "000 allow foo" -j custom_chain',
:table => 'filter',
:params => {
:jump => "custom_chain",
:action => nil,
},
},
}

# This hash is for testing converting a hash to an argument line.
HASH_TO_ARGS = {
'long_rule_1' => {
:params => {
:action => "accept",
:chain => "INPUT",
:destination => "1.1.1.1",
:dport => ["7061","7062"],
:ensure => :present,
:name => "000 allow foo",
:proto => "tcp",
:source => "1.1.1.1",
:sport => ["7061","7062"],
:table => "filter",
},
:args => ["-t", :filter, "-s", "1.1.1.1", "-d", "1.1.1.1", "-p", :tcp, "-m", "multiport", "--sports", "7061,7062", "-m", "multiport", "--dports", "7061,7062", "-m", "comment", "--comment", "000 allow foo", "-j", "ACCEPT"],
},
'long_rule_2' => {
:params => {
:chain => "INPUT",
:destination => "2.10.13.3/24",
:dport => ["7061"],
:ensure => :present,
:jump => "my_custom_chain",
:name => "700 allow bar",
:proto => "udp",
:source => "1.1.1.1",
:sport => ["7061","7062"],
:table => "filter",
},
:args => ["-t", :filter, "-s", "1.1.1.1", "-d", "2.10.13.3/24", "-p", :udp, "-m", "multiport", "--sports", "7061,7062", "-m", "multiport", "--dports", "7061", "-m", "comment", "--comment", "700 allow bar", "-j", "my_custom_chain"],
},
'no_action' => {
:params => {
:name => "100 no action",
:table => "filter",
},
:args => ["-t", :filter, "-p", :tcp, "-m", "comment", "--comment",
"100 no action"],
}
}
8 changes: 8 additions & 0 deletions spec/monkey_patches/alias_should_to_must.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'rspec'

class Object
# This is necessary because the RAL has a 'should'
# method.
alias :must :should
alias :must_not :should_not
end
11 changes: 11 additions & 0 deletions spec/monkey_patches/publicize_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Some monkey-patching to allow us to test private methods.
class Class
def publicize_methods(*methods)
saved_private_instance_methods = methods.empty? ? self.private_instance_methods : methods

self.class_eval { public(*saved_private_instance_methods) }
yield
self.class_eval { private(*saved_private_instance_methods) }
end
end

53 changes: 53 additions & 0 deletions spec/puppet_spec/files.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
require 'fileutils'
require 'tempfile'

# A support module for testing files.
module PuppetSpec::Files
# This code exists only to support tests that run as root, pretty much.
# Once they have finally been eliminated this can all go... --daniel 2011-04-08
if Puppet.features.posix? then
def self.in_tmp(path)
path =~ /^\/tmp/ or path =~ /^\/var\/folders/
end
elsif Puppet.features.microsoft_windows?
def self.in_tmp(path)
tempdir = File.expand_path(File.join(Dir::LOCAL_APPDATA, "Temp"))
path =~ /^#{tempdir}/
end
else
fail "Help! Can't find in_tmp for this platform"
end

def self.cleanup
$global_tempfiles ||= []
while path = $global_tempfiles.pop do
fail "Not deleting tmpfile #{path} outside regular tmpdir" unless in_tmp(path)

begin
FileUtils.rm_r path, :secure => true
rescue Errno::ENOENT
# nothing to do
end
end
end

def tmpfile(name)
# Generate a temporary file, just for the name...
source = Tempfile.new(name)
path = source.path
source.close!

# ...record it for cleanup,
$global_tempfiles ||= []
$global_tempfiles << File.expand_path(path)

# ...and bam.
path
end

def tmpdir(name)
path = tmpfile(name)
FileUtils.mkdir_p(path)
path
end
end
28 changes: 28 additions & 0 deletions spec/puppet_spec/fixtures.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module PuppetSpec::Fixtures
def fixtures(*rest)
File.join(PuppetSpec::FIXTURE_DIR, *rest)
end
def my_fixture_dir
callers = caller
while line = callers.shift do
next unless found = line.match(%r{/spec/(.*)_spec\.rb:})
return fixtures(found[1])
end
fail "sorry, I couldn't work out your path from the caller stack!"
end
def my_fixture(name)
file = File.join(my_fixture_dir, name)
unless File.readable? file then
fail Puppet::DevError, "fixture '#{name}' for #{my_fixture_dir} is not readable"
end
return file
end
def my_fixtures(glob = '*', flags = 0)
files = Dir.glob(File.join(my_fixture_dir, glob), flags)
unless files.length > 0 then
fail Puppet::DevError, "fixture '#{glob}' for #{my_fixture_dir} had no files!"
end
block_given? and files.each do |file| yield file end
files
end
end
87 changes: 87 additions & 0 deletions spec/puppet_spec/matchers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require 'stringio'

########################################################################
# Backward compatibility for Jenkins outdated environment.
module RSpec
module Matchers
module BlockAliases
alias_method :to, :should unless method_defined? :to
alias_method :to_not, :should_not unless method_defined? :to_not
alias_method :not_to, :should_not unless method_defined? :not_to
end
end
end


########################################################################
# Custom matchers...
RSpec::Matchers.define :have_matching_element do |expected|
match do |actual|
actual.any? { |item| item =~ expected }
end
end


RSpec::Matchers.define :exit_with do |expected|
actual = nil
match do |block|
begin
block.call
rescue SystemExit => e
actual = e.status
end
actual and actual == expected
end
failure_message_for_should do |block|
"expected exit with code #{expected} but " +
(actual.nil? ? " exit was not called" : "we exited with #{actual} instead")
end
failure_message_for_should_not do |block|
"expected that exit would not be called with #{expected}"
end
description do
"expect exit with #{expected}"
end
end


RSpec::Matchers.define :have_printed do |expected|
match do |block|
$stderr = $stdout = StringIO.new

begin
block.call
ensure
$stdout.rewind
@actual = $stdout.read

$stdout = STDOUT
$stderr = STDERR
end

if @actual then
case expected
when String
@actual.include? expected
when Regexp
expected.match @actual
else
raise ArgumentError, "No idea how to match a #{@actual.class.name}"
end
end
end

failure_message_for_should do |actual|
if actual.nil? then
"expected #{expected.inspect}, but nothing was printed"
else
"expected #{expected.inspect} to be printed; got:\n#{actual}"
end
end

description do
"expect #{expected.inspect} to be printed"
end

diffable
end
9 changes: 9 additions & 0 deletions spec/puppet_spec/verbose.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Support code for running stuff with warnings disabled.
module Kernel
def with_verbose_disabled
verbose, $VERBOSE = $VERBOSE, nil
result = yield
$VERBOSE = verbose
return result
end
end
76 changes: 76 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
dir = File.expand_path(File.dirname(__FILE__))
$LOAD_PATH.unshift File.join(dir, 'lib')

# Don't want puppet getting the command line arguments for rake or autotest
ARGV.clear

require 'puppet'
require 'mocha'
gem 'rspec', '>=2.0.0'
require 'rspec/expectations'

# So everyone else doesn't have to include this base constant.
module PuppetSpec
FIXTURE_DIR = File.join(dir = File.expand_path(File.dirname(__FILE__)), "fixtures") unless defined?(FIXTURE_DIR)
end

require 'pathname'
require 'tmpdir'

require 'puppet_spec/verbose'
require 'puppet_spec/files'
require 'puppet_spec/fixtures'
require 'puppet_spec/matchers'
require 'monkey_patches/alias_should_to_must'
require 'monkey_patches/publicize_methods'

Pathname.glob("#{dir}/shared_behaviours/**/*.rb") do |behaviour|
require behaviour.relative_path_from(Pathname.new(dir))
end

RSpec.configure do |config|
include PuppetSpec::Fixtures

config.mock_with :mocha

config.before :each do
GC.disable

# these globals are set by Application
$puppet_application_mode = nil
$puppet_application_name = nil

# REVISIT: I think this conceals other bad tests, but I don't have time to
# fully diagnose those right now. When you read this, please come tell me
# I suck for letting this float. --daniel 2011-04-21
Signal.stubs(:trap)

# Set the confdir and vardir to gibberish so that tests
# have to be correctly mocked.
Puppet[:confdir] = "/dev/null"
Puppet[:vardir] = "/dev/null"

# Avoid opening ports to the outside world
Puppet.settings[:bindaddress] = "127.0.0.1"

@logs = []
Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(@logs))

@log_level = Puppet::Util::Log.level
end

config.after :each do
Puppet.settings.clear
Puppet::Node::Environment.clear
Puppet::Util::Storage.clear
Puppet::Util::ExecutionStub.reset

PuppetSpec::Files.cleanup

@logs.clear
Puppet::Util::Log.close_all
Puppet::Util::Log.level = @log_level

GC.enable
end
end
165 changes: 165 additions & 0 deletions spec/unit/puppet/provider/iptables_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/env rspec

require 'spec_helper'
require 'puppet/provider/confine/exists'

describe 'iptables provider detection' do
let(:exists) {
Puppet::Provider::Confine::Exists
}

before :each do
# Reset the default provider
Puppet::Type.type(:firewall).defaultprovider = nil
end

it "should default to iptables provider if /sbin/iptables[-save] exists" do
# Stub lookup for /sbin/iptables & /sbin/iptables-save
exists.any_instance.stubs(:which).with("/sbin/iptables").
returns "/sbin/iptables"
exists.any_instance.stubs(:which).with("/sbin/iptables-save").
returns "/sbin/iptables-save"

# Every other command should return false so we don't pick up any
# other providers
exists.any_instance.stubs(:which).with() { |value|
! ["/sbin/iptables","/sbin/iptables-save"].include?(value)
}.returns false

# Create a resource instance and make sure the provider is iptables
resource = Puppet::Type.type(:firewall).new({
:name => '000 test foo',
})
resource.provider.class.to_s.should == "Puppet::Type::Firewall::ProviderIptables"
end

it "should raise a default provider error when there are no commands" do
# Stub all commands lookups so they return nothing
exists.any_instance.stubs(:which).returns false

# Instantiate a resource instance and make sure it raises an exception
lambda { resource = Puppet::Type.type(:firewall).new({
:name => '000 test foo' }) }.should raise_error(Puppet::DevError,
"Could not find a default provider for firewall")
end
end

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

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

it 'should be able to get a list of existing rules' do
# Pretend to return nil from iptables
provider.expects(:execute).with(['/sbin/iptables-save']).returns("")

provider.instances.each do |rule|
rule.should be_instance_of(provider)
rule.properties[:provider].to_s.should == provider.name.to_s
end
end

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

describe 'when converting rules to resources' do
ARGS_TO_HASH.each do |test_name,data|
describe "for test data '#{test_name}'" do
let(:resource) { provider.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.sort.should == data[:params].keys.sort
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_ARGS.each do |test_name,data|
describe "for test data '#{test_name}'" do
let(:resource) { Puppet::Type.type(:firewall).new(data[:params]) }
let(:provider) { Puppet::Type.type(:firewall).provider(:iptables) }
let(:instance) { provider.new(resource) }

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

describe 'when converting rules without comments to resources' do
let(:sample_rule) {
'-A INPUT -s 1.1.1.1 -d 1.1.1.1 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061, 7062 -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
resource[:name].should == "9999 #{Digest::MD5.hexdigest(resource[:line])}"
end
end

describe 'when creating resources' do
let(:instance) { provider.new(resource) }

before :each do
provider.expects(:execute).with(['/sbin/iptables-save']).returns("")
end

it 'insert_args should be an array' do
instance.insert_args.class.should == Array
end
end

describe 'when modifying resources' do
let(:instance) { provider.new(resource) }

before :each do
provider.expects(:execute).with(['/sbin/iptables-save']).returns ""
end

it 'update_args should be an array' do
instance.update_args.class.should == Array
end
end

describe 'when deleting resources' do
let(:sample_rule) {
'-A INPUT -s 1.1.1.1 -d 1.1.1.1 -p tcp -m multiport --dports 7061,7062 -m multiport --sports 7061, 7062 -j ACCEPT'
}
let(:resource) { provider.rule_to_hash(sample_rule, 'filter', 0) }
let(:instance) { provider.new(resource) }

it 'resource[:line] looks like the original rule' do
resource[:line] == sample_rule
end

it 'delete_args is an array' do
instance.delete_args.class.should == Array
end

it 'delete_args is the same as the rule string when joined' do
instance.delete_args.join(' ').should == sample_rule.gsub(/\-A/, '-D')
end
end
end
220 changes: 220 additions & 0 deletions spec/unit/puppet/type/firewall_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
#!/usr/bin/env rspec

require 'spec_helper'

firewall = Puppet::Type.type(:firewall)

describe firewall do
before :each do
@class = firewall
@provider = stub 'provider'
@provider.stubs(:name).returns(:iptables)
Puppet::Type::Firewall.stubs(:defaultprovider).returns @provider

@resource = @class.new({:name => '000 test foo'})
end

it 'should have :name be its namevar' do
@class.key_attributes.should == [:name]
end

describe ':name' do
it 'should accept a name' do
@resource[:name] = '000-test-foo'
@resource[:name].should == '000-test-foo'
end

it 'should not accept a name with non-ASCII chars' do
lambda { @resource[:name] = '%*#^(#$' }.should raise_error(Puppet::Error)
end
end

describe ':action' do
it "should have no default" do
res = @class.new(:name => "000 test")
res.parameters[:action].should == nil
end

[:accept, :drop, :reject].each do |action|
it "should accept value #{action}" do
@resource[:action] = action
@resource[:action].should == action
end
end

it 'should fail when value is not recognized' do
lambda { @resource[:action] = 'not valid' }.should raise_error(Puppet::Error)
end
end

describe ':chain' do
[:INPUT, :FORWARD, :OUTPUT, :PREROUTING, :POSTROUTING].each do |chain|
it "should accept chain value #{chain}" do
@resource[:chain] = chain
@resource[:chain].should == chain
end
end

it 'should fail when the chain value is not recognized' do
lambda { @resource[:chain] = 'not valid' }.should raise_error(Puppet::Error)
end
end

describe ':table' do
[:nat, :mangle, :filter, :raw].each do |table|
it "should accept table value #{table}" do
@resource[:table] = table
@resource[:table].should == table
end
end

it "should fail when table value is not recognized" do
lambda { @resource[:table] = 'not valid' }.should raise_error(Puppet::Error)
end
end

describe ':proto' do
[:tcp, :udp, :icmp, :esp, :ah, :vrrp, :igmp, :all].each do |proto|
it "should accept proto value #{proto}" do
@resource[:proto] = proto
@resource[:proto].should == proto
end
end

it "should fail when proto value is not recognized" do
lambda { @resource[:proto] = 'foo' }.should raise_error(Puppet::Error)
end
end

describe ':jump' do
it "should have no default" do
res = @class.new(:name => "000 test")
res.parameters[:jump].should == nil
end

['QUEUE', 'RETURN', 'DNAT', 'SNAT', 'LOG', 'MASQUERADE', 'REDIRECT'].each do |jump|
it "should accept jump value #{jump}" do
@resource[:jump] = jump
@resource[:jump].should == jump
end
end

['ACCEPT', 'DROP', 'REJECT'].each do |jump|
it "should now fail when value #{jump}" do
lambda { @resource[:jump] = jump }.should raise_error(Puppet::Error)
end
end

it "should fail when jump value is not recognized" do
lambda { @resource[:jump] = '%^&*' }.should raise_error(Puppet::Error)
end
end

[:source, :destination].each do |addr|
describe addr do
it "should accept a #{addr} as a string" do
@resource[addr] = '127.0.0.1'
@resource[addr].should == '127.0.0.1'
end
end
end

[:dport, :sport].each do |port|
describe port do
it "should accept a #{port} as string" do
@resource[port] = '22'
@resource[port].should == [22]
end

it "should accept a #{port} as an array" do
@resource[port] = ['22','23']
@resource[port].should == [22,23]
end
end
end

[:iniface, :outiface].each do |iface|
describe iface do
it "should accept #{iface} value as a string" do
@resource[iface] = 'eth1'
@resource[iface].should == 'eth1'
end
end
end

[:tosource, :todest].each do |addr|
describe addr do
it "should accept #{addr} value as a string" do
@resource[addr] = '127.0.0.1'
end
end
end

describe ':icmp' do
values = {
'0' => 'echo-reply',
'3' => 'destination-unreachable',
'4' => 'source-quench',
'6' => 'redirect',
'8' => 'echo-request',
'9' => 'router-advertisement',
'10' => 'router-solicitation',
'11' => 'time-exceeded',
'12' => 'parameter-problem',
'13' => 'timestamp-request',
'14' => 'timestamp-reply',
'17' => 'address-mask-request',
'18' => 'address-mask-reply'
}
values.each do |k,v|
it 'should convert icmp string to number' do
@resource[:icmp] = v
@resource[:icmp].should == k
end
end

it 'should accept values as integers' do
@resource[:icmp] = 9
@resource[:icmp].should == 9
end

it 'should fail if icmp type is not recognized' do
lambda { @resource[:icmp] = 'foo' }.should raise_error(Puppet::Error)
end
end

describe ':state' do
it 'should accept value as a string' do
@resource[:state] = :INVALID
@resource[:state].should == [:INVALID]
end

it 'should accept value as an array' do
@resource[:state] = [:INVALID, :NEW]
@resource[:state].should == [:INVALID, :NEW]
end
end

describe ':burst' do
it 'should accept numeric values' do
@resource[:burst] = 12
@resource[:burst].should == 12
end

it 'should fail if value is not numeric' do
lambda { @resource[:burst] = 'foo' }.should raise_error(Puppet::Error)
end
end

describe ':action and :jump' do
it 'should allow only 1 to be set at a time' do
expect {
@class.new(
:name => "001-test",
:action => "accept",
:jump => "custom_chain"
)
}.should raise_error(Puppet::Error, /^Only one of the parameters 'action' and 'jump' can be set$/)
end
end
end