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

resources { 'firewall':
Expand Down
74 changes: 37 additions & 37 deletions examples/iptables/test.pp
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
firewall { '000 allow foo':
dport => [7061, 7062],
dport => [7061, 7062],
action => accept,
proto => "tcp",
proto => 'tcp',
}

firewall { '975 log test':
state => 'NEW',
state => 'NEW',
log_level => 'panic',
jump => 'LOG'
jump => 'LOG'
}

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",
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':
Expand All @@ -31,72 +31,72 @@

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

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

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

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

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

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

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

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

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

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

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

firewall { '056 INPUT allow web in and out':
action => accept,
proto => 'tcp',
port => 80
port => 80
}

firewall { '057 INPUT limit NTP':
Expand All @@ -108,19 +108,19 @@

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

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

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

resources { 'firewall':
Expand Down
12 changes: 0 additions & 12 deletions lib/facter/iptables.rb → lib/facter/ip6tables_version.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,3 @@
Facter.add(:iptables_version) do
confine :kernel => :linux
setcode do
version = Facter::Util::Resolution.exec('iptables --version')
if version
version.match(/\d+\.\d+\.\d+/).to_s
else
nil
end
end
end

Facter.add(:ip6tables_version) do
confine :kernel => :linux
setcode do
Expand Down
11 changes: 11 additions & 0 deletions lib/facter/iptables_version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Facter.add(:iptables_version) do
confine :kernel => :linux
setcode do
version = Facter::Util::Resolution.exec('iptables --version')
if version
version.match(/\d+\.\d+\.\d+/).to_s
else
nil
end
end
end
22 changes: 18 additions & 4 deletions lib/puppet/provider/firewall/ip6tables.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,22 @@
has_feature :reject_type
has_feature :log_level
has_feature :log_prefix
has_feature :mark
has_feature :tcp_flags
has_feature :pkttype

commands :iptables => '/sbin/ip6tables'
commands :iptables_save => '/sbin/ip6tables-save'
optional_commands({
:ip6tables => '/sbin/ip6tables',
:ip6tables_save => '/sbin/ip6tables-save',
})

def self.iptables(*args)
ip6tables(*args)
end

def self.iptables_save(*args)
ip6tables_save(*args)
end

@resource_map = {
:burst => "--limit-burst",
Expand All @@ -24,7 +37,7 @@
:icmp => "-m icmp6 --icmpv6-type",
:iniface => "-i",
:jump => "-j",
:limit => "--limit",
:limit => "-m limit --limit",
:log_level => "--log-level",
:log_prefix => "--log-prefix",
:name => "-m comment --comment",
Expand All @@ -40,14 +53,15 @@
:toports => "--to-ports",
:tosource => "--to-source",
:uid => "-m owner --uid-owner",
:pkttype => "-m pkttype --pkt-type"
}

# 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, :destination, :iniface, :outiface,
:proto, :gid, :uid, :sport, :dport, :port, :name, :state, :icmp, :limit, :burst, :jump,
:proto, :gid, :uid, :sport, :dport, :port, :pkttype, :name, :state, :icmp, :limit, :burst, :jump,
:todest, :tosource, :toports, :log_level, :log_prefix, :reject]

end
109 changes: 95 additions & 14 deletions lib/puppet/provider/firewall/iptables.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,29 @@
has_feature :reject_type
has_feature :log_level
has_feature :log_prefix
has_feature :mark
has_feature :tcp_flags
has_feature :pkttype
has_feature :socket

commands :iptables => '/sbin/iptables'
commands :iptables_save => '/sbin/iptables-save'
optional_commands({
:iptables => '/sbin/iptables',
:iptables_save => '/sbin/iptables-save',
})

defaultfor :kernel => :linux

iptables_version = Facter.fact('iptables_version').value
if (iptables_version and Puppet::Util::Package.versioncmp(iptables_version, '1.4.1') < 0)
mark_flag = '--set-mark'
else
mark_flag = '--set-xmark'
end

@resource_map = {
:burst => "--limit-burst",
:destination => "-d",
:dport => "-m multiport --dports",
:dport => ["-m multiport --dports", "-m (udp|tcp) --dport"],
:gid => "-m owner --gid-owner",
:icmp => "-m icmp --icmp-type",
:iniface => "-i",
Expand All @@ -39,23 +52,27 @@
:port => '-m multiport --ports',
:proto => "-p",
:reject => "--reject-with",
:set_mark => mark_flag,
:socket => "-m socket",
:source => "-s",
:sport => ["-m multiport --sports", "-m (udp|tcp) --sport"],
:state => "-m state --state",
:sport => "-m multiport --sports",
:table => "-t",
:tcp_flags => "-m tcp --tcp-flags",
:todest => "--to-destination",
:toports => "--to-ports",
:tosource => "--to-source",
:uid => "-m owner --uid-owner",
:pkttype => "-m pkttype --pkt-type"
}

# 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, :destination, :iniface, :outiface,
:proto, :gid, :uid, :sport, :dport, :port, :name, :state, :icmp, :limit, :burst,
:jump, :todest, :tosource, :toports, :log_level, :log_prefix, :reject]
:proto, :tcp_flags, :gid, :uid, :sport, :dport, :port, :socket, :pkttype, :name, :state, :icmp, :limit, :burst,
:jump, :todest, :tosource, :toports, :log_level, :log_prefix, :reject, :set_mark]

def insert
debug 'Inserting rule %s' % resource[:name]
Expand Down Expand Up @@ -94,7 +111,7 @@ def self.instances

# 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/
unless line =~ /^\#\s+|^\:\S+|^COMMIT|^FATAL/
if line =~ /^\*/
table = line.sub(/\*/, "")
else
Expand All @@ -113,22 +130,69 @@ 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]

####################
# 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"')

# Trick the system for booleans
known_booleans.each do |bool|
values = values.sub(/#{@resource_map[bool]}/, '-m socket true')
end

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

# Here we iterate across our values to generate an array of keys
@resource_list.reverse.each do |k|
if values.slice!(/\s#{@resource_map[k]}/)
keys << k
resource_map_key = @resource_map[k]
[resource_map_key].flatten.each do |opt|
if values.slice!(/\s#{opt}/)
keys << k
break
end
end
end

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

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

#####################
# POST PARSE CLUDGING
#####################

# Normalise all rules to CIDR notation.
[:source, :destination].each do |prop|
hash[prop] = Puppet::Util::IPCidr.new(hash[prop]).cidr unless hash[prop].nil?
end

[:dport, :sport, :port, :state].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|
if hash[bool] != nil then
if hash[bool] == "true" then
hash[bool] = true
else
raise "Parser error: #{bool} was meant to be a boolean but received value: #{hash[bool]}."
end
end
end

# Our type prefers hyphens over colons for ranges so ...
# Iterate across all ports replacing colons with hyphens so that ranges match
# the types expectations.
Expand Down Expand Up @@ -211,10 +275,14 @@ def delete_args
end
end

line.unshift("-t", properties[:table])

# Return array without nils
line.compact
end

# This method takes the resource, and attempts to generate the command line
# arguments for iptables.
def general_args
debug "Current resource: %s" % resource.class

Expand All @@ -226,14 +294,18 @@ def general_args
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
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(' ')
args << [resource_map[res]].flatten.first.split(' ')

# For sport and dport, convert hyphens to colons since the type
# expects hyphens for ranges of ports.
Expand All @@ -243,9 +315,16 @@ def general_args
end
end

if resource_value.is_a?(Array)
# our tcp_flags takes a single string with comma lists separated
# by space
# --tcp-flags expects two arguments
if res == :tcp_flags
one, two = resource_value.split(' ')
args << one
args << two
elsif resource_value.is_a?(Array)
args << resource_value.join(',')
else
elsif !resource_value.nil?
args << resource_value
end
end
Expand All @@ -257,9 +336,11 @@ def insert_order
debug("[insert_order]")
rules = []

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

# No rules at all? Just bail now.
Expand Down
164 changes: 164 additions & 0 deletions lib/puppet/provider/firewallchain/iptables_chain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
Puppet::Type.type(:firewallchain).provide :iptables_chain do
@doc = "Iptables chain provider"

has_feature :iptables_chain
has_feature :policy

optional_commands({
:iptables => '/sbin/iptables',
:iptables_save => '/sbin/iptables-save',
:ip6tables => '/sbin/ip6tables',
:ip6tables_save => '/sbin/ip6tables-save',
:ebtables => '/sbin/ebtables',
:ebtables_save => '/sbin/ebtables-save',
})

defaultfor :kernel => :linux

# chain name is greedy so we anchor from the end.
# [\d+:\d+] doesn't exist on ebtables
Mapping = {
:IPv4 => {
:tables => method(:iptables),
:save => method(:iptables_save),
:re => /^:(.+)\s(\S+)\s\[\d+:\d+\]$/,
},
:IPv6 => {
:tables => method(:ip6tables),
:save => method(:ip6tables_save),
:re => /^:(.+)\s(\S+)\s\[\d+:\d+\]$/,
},
:ethernet => {
:tables => method(:ebtables),
:save => method(:ebtables_save),
:re => /^:(.+)\s(\S+)$/,
}
}
InternalChains = /^(PREROUTING|POSTROUTING|BROUTING|INPUT|FORWARD|OUTPUT)$/
Tables = 'nat|mangle|filter|raw|rawpost|broute'
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 properties[:ensure] == protocol
debug "Skipping Inserting chain #{chain} on table #{table} (#{protocol}) already exists"
else
debug "Inserting chain #{chain} on table #{table} (#{protocol}) using #{t}"
t.call ['-t',table,'-N',chain]
unless @resource[:policy].nil?
t.call ['-t',table,'-P',chain,@resource[:policy].to_s.upcase]
end
end
end
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|
debug "Deleting chain #{chain} on table #{table}"
t.call ['-t',table,'-X',chain]
end
end

def exists?
properties[:ensure] == :present
end

def policy=(value)
return if value == :empty
allvalidchains do |t, chain, table|
p = ['-t',table,'-P',chain,value.to_s.upcase]
debug "[set policy] #{t} #{p}"
t.call p
end
end

def policy
debug "[get policy] #{@resource[:name]} =#{@property_hash[:policy].to_s.downcase}"
return @property_hash[:policy].to_s.downcase
end

def self.prefetch(resources)
debug("[prefetch(resources)]")
instances.each do |prov|
if resource = resources[prov.name]
resource.provider = prov
end
end
end

def flush
debug("[flush]")
# Clear the property hash so we re-initialize with updated values
@property_hash.clear
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}
end
@property_hash.dup
end

# Pull the current state of the list from the full list.
def query
self.class.instances.each do |instance|
if instance.name == self.name
debug "query found #{self.name}" % instance.properties.inspect
return instance.properties
end
end
nil
end

def self.instances
debug "[instances]"
table = nil
chains = []

Mapping.each { |p, c|
begin
c[:save].call.each_line do |line|
if line =~ c[:re] then
name = $1 + ':' + (table == 'filter' ? 'filter' : table) + ':' + p.to_s
policy = $2 == '-' ? nil : $2.downcase.to_sym

chains << new({
:name => name,
:policy => policy,
:ensure => :present,
})

debug "[instance] '#{name}' #{policy}"
elsif line =~ /^\*(\S+)/
table = $1
else
next
end
end
rescue Puppet::Error
# ignore command not found for ebtables or anything that doesn't exist
end
}

chains
end

def allvalidchains
@resource[:name].match(Nameformat)
chain = $1
table = $2
protocol = $3
yield Mapping[protocol.to_sym][:tables],chain,table,protocol.to_sym
end

end
180 changes: 167 additions & 13 deletions lib/puppet/type/firewall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
@doc = <<-EOS
This type provides the capability to manage firewall rules within
puppet.
**Autorequires:** If Puppet is managing the iptables or ip6tables chains
specified in the `chain` or `jump` parameters, the firewall resource
will autorequire those firewallchain resources.
EOS

feature :rate_limiting, "Rate limiting features."
Expand All @@ -27,6 +31,10 @@
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"
feature :mark, "Set the netfilter mark value associated with the packet"
feature :tcp_flags, "The ability to match on particular TCP flag settings"
feature :pkttype, "Match a packet type"
feature :socket, "Match open sockets"

# provider specific features
feature :iptables, "The provider provides iptables features."
Expand Down Expand Up @@ -81,7 +89,7 @@
# Generic matching properties
newproperty(:source) do
desc <<-EOS
An array of source addresses. For example:
The source address. For example:
source => '192.168.2.0/24'
Expand All @@ -95,7 +103,7 @@

newproperty(:destination) do
desc <<-EOS
An array of destination addresses to match. For example:
The destination address to match. For example:
destination => '192.168.1.0/24'
Expand Down Expand Up @@ -124,7 +132,11 @@
EOS

munge do |value|
@resource.string_to_port(value)
@resource.string_to_port(value, :proto)
end

def is_to_s(value)
should_to_s(value)
end

def should_to_s(value)
Expand All @@ -150,7 +162,11 @@ def should_to_s(value)
EOS

munge do |value|
@resource.string_to_port(value)
@resource.string_to_port(value, :proto)
end

def is_to_s(value)
should_to_s(value)
end

def should_to_s(value)
Expand All @@ -176,7 +192,11 @@ def should_to_s(value)
EOS

munge do |value|
@resource.string_to_port(value)
@resource.string_to_port(value, :proto)
end

def is_to_s(value)
should_to_s(value)
end

def should_to_s(value)
Expand All @@ -191,10 +211,26 @@ def should_to_s(value)
*tcp*.
EOS

newvalues(:tcp, :udp, :icmp, :"ipv6-icmp", :esp, :ah, :vrrp, :igmp, :ipencap, :all)
newvalues(:tcp, :udp, :icmp, :"ipv6-icmp", :esp, :ah, :vrrp, :igmp, :ipencap, :ospf, :gre, :all)
defaultto "tcp"
end

# tcp-specific
newproperty(:tcp_flags, :required_features => :tcp_flags) do
desc <<-EOS
Match when the TCP flags are as specified.
Is a string with a list of comma-separated flag names for the mask,
then a space, then a comma-separated list of flags that should be set.
The flags are: SYN ACK FIN RST URG PSH ALL NONE
Note that you specify them in the order that iptables --list-rules
would list them to avoid having puppet think you changed the flags.
Example: FIN,SYN,RST,ACK SYN matches packets with the SYN bit set and the
ACK,RST and FIN bits cleared. Such packets are used to request
TCP connection initiation.
EOS
end


# Iptables specific
newproperty(:chain, :required_features => :iptables) do
desc <<-EOS
Expand Down Expand Up @@ -243,6 +279,7 @@ def should_to_s(value)
* LOG
* MASQUERADE
* REDIRECT
* MARK
But any valid chain name is allowed.
Expand All @@ -264,7 +301,7 @@ def should_to_s(value)

if ["accept","reject","drop"].include?(value.downcase)
raise ArgumentError, <<-EOS
Jump destination should not be one of ACCEPT, REJECT or DENY. Use
Jump destination should not be one of ACCEPT, REJECT or DROP. Use
the action property instead.
EOS
end
Expand All @@ -277,14 +314,14 @@ def should_to_s(value)
desc <<-EOS
Input interface to filter on.
EOS
newvalues(/^[a-zA-Z0-9\-_]+$/)
newvalues(/^[a-zA-Z0-9\-\._\+]+$/)
end

newproperty(:outiface, :required_features => :interface_match) do
desc <<-EOS
Output interface to filter on.
EOS
newvalues(/^[a-zA-Z0-9\-_]+$/)
newvalues(/^[a-zA-Z0-9\-\._\+]+$/)
end

# NAT specific properties
Expand Down Expand Up @@ -348,11 +385,32 @@ def should_to_s(value)
newproperty(:icmp, :required_features => :icmp_match) do
desc <<-EOS
When matching ICMP packets, this is the type of ICMP packet to match.
A value of "any" is not supported. To achieve this behaviour the
parameter should simply be omitted or undefined.
EOS

validate do |value|
if value == "any"
raise ArgumentError,
"Value 'any' is not valid. This behaviour should be achieved " \
"by omitting or undefining the ICMP parameter."
end
end

munge do |value|
if value.kind_of?(String)
value = @resource.icmp_name_to_number(value)
# ICMP codes differ between IPv4 and IPv6.
case @resource[:provider]
when :iptables
protocol = 'inet'
when :ip6tables
protocol = 'inet6'
else
self.fail("cannot work out protocol family")
end

value = @resource.icmp_name_to_number(value, protocol)
else
value
end
Expand Down Expand Up @@ -382,7 +440,11 @@ def should_to_s(value)
# 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
@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)
Expand All @@ -408,28 +470,105 @@ def should_to_s(value)
newvalue(/^\d+$/)
end

newproperty(:uid, :array_matching =>:all, :required_features => :owner) do
newproperty(:uid, :required_features => :owner) do
desc <<-EOS
UID or Username owner matching rule. Accepts a string argument
only, as iptables does not accept multiple uid in a single
statement.
EOS
end

newproperty(:gid, :array_matching =>:all, :required_features => :owner) do
newproperty(:gid, :required_features => :owner) do
desc <<-EOS
GID or Group owner matching rule. Accepts a string argument
only, as iptables does not accept multiple gid in a single
statement.
EOS
end

newproperty(:set_mark, :required_features => :mark) do
desc <<-EOS
Set the Netfilter mark value associated with the packet. Accepts either of:
mark/mask or mark. These will be converted to hex if they are not already.
EOS

munge do |value|
int_or_hex = '[a-fA-F0-9x]'
match = value.to_s.match("(#{int_or_hex}+)(/)?(#{int_or_hex}+)?")
mark = @resource.to_hex32(match[1])

# Values that can't be converted to hex.
# Or contain a trailing slash with no mask.
if mark.nil? or (mark and match[2] and match[3].nil?)
raise ArgumentError, "MARK value must be integer or hex between 0 and 0xffffffff"
end

# Old iptables does not support a mask. New iptables will expect one.
iptables_version = Facter.fact('iptables_version').value
mask_required = (iptables_version and Puppet::Util::Package.versioncmp(iptables_version, '1.4.1') >= 0)

if mask_required
if match[3].nil?
value = "#{mark}/0xffffffff"
else
mask = @resource.to_hex32(match[3])
if mask.nil?
raise ArgumentError, "MARK mask must be integer or hex between 0 and 0xffffffff"
end
value = "#{mark}/#{mask}"
end
else
unless match[3].nil?
raise ArgumentError, "iptables version #{iptables_version} does not support masks on MARK rules"
end
value = mark
end

value
end
end

newproperty(:pkttype, :required_features => :pkttype) do
desc <<-EOS
Sets the packet type to match.
EOS

newvalues(:unicast, :broadcast, :multicast)
end

newproperty(:socket, :required_features => :socket) do
desc <<-EOS
If true, matches if an open socket can be found by doing a coket lookup
on the packet.
EOS

newvalues(:true, :false)
end

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

autorequire(:firewallchain) do
case value(:provider)
when :iptables
protocol = "IPv4"
when :ip6tables
protocol = "IPv6"
else
return
end

reqs = []
[value(:chain), value(:jump)].each do |chain|
reqs << "#{chain}:#{value(:table)}:#{protocol}" unless chain.nil?
end

reqs
end

validate do
debug("[validate]")

Expand Down Expand Up @@ -488,6 +627,15 @@ def should_to_s(value)
end
end

if value(:set_mark)
unless value(:jump).to_s =~ /MARK/ &&
value(:chain).to_s =~ /PREROUTING|OUTPUT/ &&
value(:table).to_s =~ /mangle/
self.fail "Parameter set_mark only applies to " \
"the PREROUTING or OUTPUT chain of the mangle table and when jump => MARK"
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 " \
Expand Down Expand Up @@ -529,6 +677,12 @@ def should_to_s(value)
end
end

if value(:log_prefix) || value(:log_level)
unless value(:jump).to_s == "LOG"
self.fail "Parameter log_prefix and log_level require jump => LOG"
end
end

if value(:burst) && ! value(:limit)
self.fail "burst makes no sense without limit"
end
Expand Down
125 changes: 125 additions & 0 deletions lib/puppet/type/firewallchain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
Puppet::Type.newtype(:firewallchain) do

@doc = <<-EOS
This type provides the capability to manage rule chains for firewalls.
Currently this supports only iptables, ip6tables and ebtables on Linux. And
provides support for setting the default policy on chains and tables that
allow it.
EOS

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

ensurable do
defaultvalues
defaultto :present
end

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

validate do |value|
if value !~ Nameformat then
raise ArgumentError, "Inbuilt chains must be in the form {chain}:{table}:{protocol} where {table} is one of FILTER, NAT, MANGLE, RAW, RAWPOST, BROUTE or empty (alias for filter), chain can be anything without colons or one of PREROUTING, POSTROUTING, BROUTING, INPUT, FORWARD, OUTPUT for the inbuilt chains, and {protocol} being IPv4, IPv6, ethernet (ethernet bridging) got '#{value}' table:'#{$1}' chain:'#{$2}' protocol:'#{$3}'"
else
chain = $1
table = $2
protocol = $3
case table
when 'filter'
if chain =~ /^(PREROUTING|POSTROUTING|BROUTING)$/
raise ArgumentError, "INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table 'filter'"
end
when 'mangle'
if chain =~ InternalChains && chain == 'BROUTING'
raise ArgumentError, "PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table 'mangle'"
end
when 'nat'
if chain =~ /^(BROUTING|INPUT|FORWARD)$/
raise ArgumentError, "PREROUTING, POSTROUTING 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"
end
when 'raw'
if chain =~ /^(POSTROUTING|BROUTING|INPUT|FORWARD)$/
raise ArgumentError,'PREROUTING and OUTPUT are the only inbuilt chains in the table \'raw\''
end
when 'broute'
if protocol != 'ethernet'
raise ArgumentError,'BROUTE is only valid with protocol \'ethernet\''
end
if chain =~ /^PREROUTING|POSTROUTING|INPUT|FORWARD|OUTPUT$/
raise ArgumentError,'BROUTING is the only inbuilt chain allowed on on table \'broute\''
end
end
if chain == 'BROUTING' && ( protocol != 'ethernet' || table!='broute')
raise ArgumentError,'BROUTING is the only inbuilt chain allowed on on table \'BROUTE\' with protocol \'ethernet\' i.e. \'broute:BROUTING:enternet\''
end
end
end
end

newproperty(:policy) do
desc <<-EOS
This is the action to when the end of the chain is reached.
It can only be set on inbuilt chains (INPUT, FORWARD, OUTPUT,
PREROUTING, POSTROUTING) and can be one of:
* accept - the packet is accepted
* drop - the packet is dropped
* queue - the packet is passed userspace
* return - the packet is returned to calling (jump) queue
or the default of inbuilt chains
EOS
newvalues(:accept, :drop, :queue, :return)
defaultto do
# ethernet chain have an ACCEPT default while other haven't got an
# allowed value
if @resource[:name] =~ /:ethernet$/
:accept
else
nil
end
end
end

validate do
debug("[validate]")

value(:name).match(Nameformat)
chain = $1
table = $2
protocol = $3

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

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

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

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

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

self.fail 'The "nat" table is not intended for filtering, the use of DROP is therefore inhibited'
end
end
end
71 changes: 62 additions & 9 deletions lib/puppet/util/firewall.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
# Util module for puppetlabs-firewall
module Puppet::Util::Firewall
# Translate the symbolic names for icmp packet types to integers
def icmp_name_to_number(value_icmp)
def icmp_name_to_number(value_icmp, protocol)
if value_icmp =~ /\d{1,2}$/
value_icmp
else
elsif protocol == 'inet'
case value_icmp
when "echo-reply" then "0"
when "destination-unreachable" then "3"
Expand All @@ -25,6 +25,20 @@ def icmp_name_to_number(value_icmp)
when "address-mask-reply" then "18"
else nil
end
elsif protocol == 'inet6'
case value_icmp
when "destination-unreachable" then "1"
when "time-exceeded" then "3"
when "parameter-problem" then "4"
when "echo-request" then "128"
when "echo-reply" then "129"
when "router-solicitation" then "133"
when "router-advertisement" then "134"
when "redirect" then "137"
else nil
end
else
raise ArgumentError, "unsupported protocol family '#{protocol}'"
end
end

Expand All @@ -51,29 +65,68 @@ def log_level_name_to_number(value)
end
end

# This method takes a string and attempts to convert it to a port number
# if valid.
# This method takes a string and a protocol and attempts to convert
# it to a port number if valid.
#
# If the string already contains a port number or perhaps a range of ports
# in the format 22:1000 for example, it simply returns the string and does
# nothing.
def string_to_port(value)
def string_to_port(value, proto)
proto = proto.to_s
unless proto =~ /^(tcp|udp)$/
proto = 'tcp'
end

if value.kind_of?(String)
if value.match(/^\d+(-\d+)?$/)
return value
else
return Socket.getservbyname(value).to_s
return Socket.getservbyname(value, proto).to_s
end
else
Socket.getservbyname(value)
Socket.getservbyname(value.to_s, proto).to_s
end
end

# Takes an address and returns it in CIDR notation.
#
# If the address is:
#
# - A hostname:
# It will be resolved
# - An IPv4 address:
# It will be qualified with a /32 CIDR notation
# - An IPv6 address:
# It will be qualified with a /128 CIDR notation
# - An IP address with a CIDR notation:
# It will be normalised
# - An IP address with a dotted-quad netmask:
# It will be converted to CIDR notation
# - Any address with a resulting prefix length of zero:
# It will return nil which is equivilent to not specifying an address
#
def host_to_ip(value)
begin
Puppet::Util::IPCidr.new(value).cidr
value = Puppet::Util::IPCidr.new(value)
rescue
Puppet::Util::IPCidr.new(Resolv.getaddress(value)).cidr
value = Puppet::Util::IPCidr.new(Resolv.getaddress(value))
end

return nil if value.prefixlen == 0
value.cidr
end

# Validates the argument is int or hex, and returns valid hex
# conversion of the value or nil otherwise.
def to_hex32(value)
begin
value = Integer(value)
if value.between?(0, 0xffffffff)
return '0x' + value.to_s(16)
end
rescue ArgumentError
# pass
end
return nil
end
end
11 changes: 11 additions & 0 deletions lib/puppet/util/ipcidr.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@
module Puppet
module Util
class IPCidr < IPAddr
def initialize(ipaddr)
begin
super(ipaddr)
rescue ArgumentError => e
if e.message =~ /invalid address/
raise ArgumentError, "Invalid address from IPAddr.new: #{ipaddr}"
else
raise e
end
end
end

def netmask
_to_string(@mask_addr)
Expand Down
361 changes: 359 additions & 2 deletions spec/fixtures/iptables/conversion_hash.rb

Large diffs are not rendered by default.

8 changes: 0 additions & 8 deletions spec/monkey_patches/alias_should_to_must.rb

This file was deleted.

11 changes: 0 additions & 11 deletions spec/monkey_patches/publicize_methods.rb

This file was deleted.

53 changes: 0 additions & 53 deletions spec/puppet_spec/files.rb

This file was deleted.

28 changes: 0 additions & 28 deletions spec/puppet_spec/fixtures.rb

This file was deleted.

87 changes: 0 additions & 87 deletions spec/puppet_spec/matchers.rb

This file was deleted.

9 changes: 0 additions & 9 deletions spec/puppet_spec/verbose.rb

This file was deleted.

67 changes: 3 additions & 64 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,79 +4,18 @@
# 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'
require 'rubygems'
require 'bundler/setup'

# 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
Bundler.require :default, :test

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 = []
# This tests allows the spec_helper to be >2.6.7 and >2.7.1 compatible
# as the Puppet::Test::LogCollector facility wasn't available until 2.7.x
if Puppet.const_defined?("Test") and Puppet::Test.const_defined?("LogCollector")
Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(@logs))
else
Puppet::Util::Log.newdestination(@logs)
end

@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
2 changes: 1 addition & 1 deletion spec/unit/facter/iptables_spec.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
require 'spec_helper'
require 'facter/iptables'

describe "Facter::Util::Fact" do
before {
Facter.clear
Facter.fact(:kernel).stubs(:value).returns("Linux")
Facter.fact(:kernelrelease).stubs(:value).returns("2.6")
}
Expand Down
217 changes: 217 additions & 0 deletions spec/unit/puppet/provider/iptables_chain_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
#!/usr/bin/env rspec

require 'spec_helper'
require 'puppet'

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

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

it "should default to iptables provider if /sbin/(eb|ip|ip6)tables[-save] exists" do
# Stub lookup for /sbin/iptables & /sbin/iptables-save
exists.any_instance.stubs(:which).with("/sbin/ebtables").
returns "/sbin/ebtables"
exists.any_instance.stubs(:which).with("/sbin/ebtables-save").
returns "/sbin/ebtables-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"

exists.any_instance.stubs(:which).with("/sbin/ip6tables").
returns "/sbin/ip6tables"
exists.any_instance.stubs(:which).with("/sbin/ip6tables-save").
returns "/sbin/ip6tables-save"

# Every other command should return false so we don't pick up any
# other providers
exists.any_instance.stubs(:which).with() { |value|
value !~ /\/sbin\/(eb|ip|ip6)tables(-save)?$/
}.returns false

# Create a resource instance and make sure the provider is iptables
resource = Puppet::Type.type(:firewallchain).new({
:name => 'test:filter:IPv4',
})
resource.provider.class.to_s.should == "Puppet::Type::Firewallchain::ProviderIptables_chain"
end
end

describe 'iptables chain provider' do
let(:provider) { Puppet::Type.type(:firewallchain).provider(:iptables_chain) }
let(:resource) {
Puppet::Type.type(:firewallchain).new({
:name => ':test:',
})
}

before :each do
Puppet::Type::Firewallchain.stubs(:defaultprovider).returns provider
provider.stubs(:command).with(:ebtables_save).returns "/sbin/ebtables-save"
provider.stubs(:command).with(:iptables_save).returns "/sbin/iptables-save"
provider.stubs(:command).with(:ip6tables_save).returns "/sbin/ip6tables-save"
end

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

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

end

describe 'iptables chain resource parsing' do
let(:provider) { Puppet::Type.type(:firewallchain).provider(:iptables_chain) }

before :each do
ebtables = ['BROUTE:BROUTING:ethernet',
'BROUTE:broute:ethernet',
':INPUT:ethernet',
':FORWARD:ethernet',
':OUTPUT:ethernet',
':filter:ethernet',
':filterdrop:ethernet',
':filterreturn:ethernet',
'NAT:PREROUTING:ethernet',
'NAT:OUTPUT:ethernet',
'NAT:POSTROUTING:ethernet',
]
provider.stubs(:execute).with(['/sbin/ebtables-save']).returns('
*broute
:BROUTING ACCEPT
:broute ACCEPT
*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
:filter ACCEPT
:filterdrop DROP
:filterreturn RETURN
*nat
:PREROUTING ACCEPT
:OUTPUT ACCEPT
:POSTROUTING ACCEPT
')

iptables = [
'raw:PREROUTING:IPv4',
'raw:OUTPUT:IPv4',
'raw:raw:IPv4',
'mangle:PREROUTING:IPv4',
'mangle:INPUT:IPv4',
'mangle:FORWARD:IPv4',
'mangle:OUTPUT:IPv4',
'mangle:POSTROUTING:IPv4',
'mangle:mangle:IPv4',
'NAT:PREROUTING:IPv4',
'NAT:OUTPUT:IPv4',
'NAT:POSTROUTING:IPv4',
'NAT:mangle:IPv4',
'NAT:mangle:IPv4',
'NAT:mangle:IPv4',
':$5()*&%\'"^$): :IPv4',
]
provider.stubs(:execute).with(['/sbin/iptables-save']).returns('
# Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012
*raw
:PREROUTING ACCEPT [12:1780]
:OUTPUT ACCEPT [19:1159]
:raw - [0:0]
COMMIT
# Completed on Mon Jan 2 01:20:06 2012
# Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012
*mangle
:PREROUTING ACCEPT [12:1780]
:INPUT ACCEPT [12:1780]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [19:1159]
:POSTROUTING ACCEPT [19:1159]
:mangle - [0:0]
COMMIT
# Completed on Mon Jan 2 01:20:06 2012
# Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012
*nat
:PREROUTING ACCEPT [2242:639750]
:OUTPUT ACCEPT [5176:326206]
:POSTROUTING ACCEPT [5162:325382]
COMMIT
# Completed on Mon Jan 2 01:20:06 2012
# Generated by iptables-save v1.4.9 on Mon Jan 2 01:20:06 2012
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [5673:420879]
:$5()*&%\'"^$): - [0:0]
COMMIT
# Completed on Mon Jan 2 01:20:06 2012
')
ip6tables = [
'raw:PREROUTING:IPv6',
'raw:OUTPUT:IPv6',
'raw:ff:IPv6',
'mangle:PREROUTING:IPv6',
'mangle:INPUT:IPv6',
'mangle:FORWARD:IPv6',
'mangle:OUTPUT:IPv6',
'mangle:POSTROUTING:IPv6',
'mangle:ff:IPv6',
':INPUT:IPv6',
':FORWARD:IPv6',
':OUTPUT:IPv6',
':test:IPv6',
]
provider.stubs(:execute).with(['/sbin/ip6tables-save']).returns('
# Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012
*raw
:PREROUTING ACCEPT [2173:489241]
:OUTPUT ACCEPT [0:0]
:ff - [0:0]
COMMIT
# Completed on Mon Jan 2 01:31:39 2012
# Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012
*mangle
:PREROUTING ACCEPT [2301:518373]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:ff - [0:0]
COMMIT
# Completed on Mon Jan 2 01:31:39 2012
# Generated by ip6tables-save v1.4.9 on Mon Jan 2 01:31:39 2012
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [20:1292]
:test - [0:0]
COMMIT
# Completed on Mon Jan 2 01:31:39 2012
')
@all = ebtables + iptables + ip6tables
# IPv4 and IPv6 names also exist as resources {table}:{chain}:IP and {table}:{chain}:
iptables.each { |name| @all += [ name[0..-3], name[0..-5] ] }
ip6tables.each { |name| @all += [ name[0..-3], name[0..-5] ] }
end

it 'should have all in parsed resources' do
provider.instances.each do |resource|
@all.include?(resource.name)
end
end

end
42 changes: 18 additions & 24 deletions spec/unit/puppet/provider/iptables_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,6 @@
})
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
Expand All @@ -56,18 +46,29 @@
before :each do
Puppet::Type::Firewall.stubs(:defaultprovider).returns provider
provider.stubs(:command).with(:iptables_save).returns "/sbin/iptables-save"

# Stub iptables version
Facter.fact(:iptables_version).stubs(:value).returns("1.4.2")

Puppet::Util::Execution.stubs(:execute).returns ""
Puppet::Util.stubs(:which).with("/sbin/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("")

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

it 'should ignore lines with fatal errors' do
Puppet::Util::Execution.stubs(:execute).with(['/sbin/iptables-save']).
returns("FATAL: Could not load /lib/modules/2.6.18-028stab095.1/modules.dep: No such file or directory")

provider.instances.length.should == 0
end

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

Expand All @@ -79,7 +80,7 @@
# 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
resource.keys.should =~ data[:params].keys
end
end

Expand Down Expand Up @@ -122,10 +123,6 @@
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
Expand All @@ -134,10 +131,6 @@
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
Expand All @@ -159,7 +152,8 @@
end

it 'delete_args is the same as the rule string when joined' do
instance.delete_args.join(' ').should == sample_rule.gsub(/\-A/, '-D')
instance.delete_args.join(' ').should == sample_rule.gsub(/\-A/,
'-t filter -D')
end
end
end
270 changes: 236 additions & 34 deletions spec/unit/puppet/type/firewall_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
Puppet::Type::Firewall.stubs(:defaultprovider).returns @provider

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

# Stub iptables version
Facter.fact(:iptables_version).stubs(:value).returns("1.4.2")
end

it 'should have :name be its namevar' do
Expand All @@ -34,7 +37,7 @@
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
Expand Down Expand Up @@ -74,7 +77,7 @@
end

describe ':proto' do
[:tcp, :udp, :icmp, :esp, :ah, :vrrp, :igmp, :ipencap, :all].each do |proto|
[:tcp, :udp, :icmp, :esp, :ah, :vrrp, :igmp, :ipencap, :ospf, :gre, :all].each do |proto|
it "should accept proto value #{proto}" do
@resource[:proto] = proto
@resource[:proto].should == proto
Expand All @@ -92,7 +95,7 @@
res.parameters[:jump].should == nil
end

['QUEUE', 'RETURN', 'DNAT', 'SNAT', 'LOG', 'MASQUERADE', 'REDIRECT'].each do |jump|
['QUEUE', 'RETURN', 'DNAT', 'SNAT', 'LOG', 'MASQUERADE', 'REDIRECT', 'MARK'].each do |jump|
it "should accept jump value #{jump}" do
@resource[:jump] = jump
@resource[:jump].should == jump
Expand All @@ -116,6 +119,12 @@
@resource[addr] = '127.0.0.1'
@resource[addr].should == '127.0.0.1/32'
end
['0.0.0.0/0', '::/0'].each do |prefix|
it "should be nil for zero prefix length address #{prefix}" do
@resource[addr] = prefix
@resource[addr].should == nil
end
end
end
end

Expand All @@ -131,6 +140,11 @@
@resource[port].should == ['22','23']
end

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

it "should accept a #{port} as a hyphen separated range" do
@resource[port] = ['22-1000']
@resource[port].should == ['22-1000']
Expand All @@ -149,11 +163,11 @@
end

it "should not accept something invalid for #{port}" do
expect { @resource[port] = 'something odd' }.should raise_error(Puppet::Error, /^Parameter .+ failed: Munging failed for value ".+" in class .+: no such service/)
expect { @resource[port] = 'something odd' }.to raise_error(Puppet::Error, /^Parameter .+ failed.+Munging failed for value ".+" in class .+: no such service/)
end

it "should not accept something invalid in an array for #{port}" do
expect { @resource[port] = ['something odd','something even odder'] }.should raise_error(Puppet::Error, /^Parameter .+ failed: Munging failed for value ".+" in class .+: no such service/)
expect { @resource[port] = ['something odd','something even odder'] }.to raise_error(Puppet::Error, /^Parameter .+ failed.+Munging failed for value ".+" in class .+: no such service/)
end
end
end
Expand Down Expand Up @@ -205,25 +219,43 @@
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'
icmp_codes = {
:iptables => {
'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'
},
:ip6tables => {
'1' => 'destination-unreachable',
'3' => 'time-exceeded',
'4' => 'parameter-problem',
'128' => 'echo-request',
'129' => 'echo-reply',
'133' => 'router-solicitation',
'134' => 'router-advertisement',
'137' => 'redirect'
}
}
values.each do |k,v|
it 'should convert icmp string to number' do
@resource[:icmp] = v
@resource[:icmp].should == k
icmp_codes.each do |provider, values|
describe provider do
values.each do |k,v|
it 'should convert icmp string to number' do
@resource[:provider] = provider
@resource[:provider].should == provider
@resource[:icmp] = v
@resource[:icmp].should == k
end
end
end
end

Expand All @@ -232,7 +264,11 @@
@resource[:icmp].should == 9
end

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

it 'should fail if icmp type cannot be mapped to a numeric' do
lambda { @resource[:icmp] = 'foo' }.should raise_error(Puppet::Error)
end
end
Expand Down Expand Up @@ -267,31 +303,197 @@

describe ':action and :jump' do
it 'should allow only 1 to be set at a time' do
expect {
expect {
@class.new(
:name => "001-test",
:action => "accept",
:name => "001-test",
:action => "accept",
:jump => "custom_chain"
)
}.should raise_error(Puppet::Error, /^Only one of the parameters 'action' and 'jump' can be set$/)
}.to raise_error(Puppet::Error, /^Only one of the parameters 'action' and 'jump' can be set$/)
end
end
describe ':gid and :uid' do
it 'should allow me to set uid' do
@resource[:uid] = 'root'
@resource[:uid].should == ['root']
@resource[:uid].should == 'root'
end
it 'should allow me to set uid as an array, breaking iptables' do
it 'should allow me to set uid as an array, and silently hide my error' do
@resource[:uid] = ['root', 'bobby']
@resource[:uid].should == ['root', 'bobby']
@resource[:uid].should == 'root'
end
it 'should allow me to set gid' do
@resource[:gid] = 'root'
@resource[:gid].should == ['root']
@resource[:gid].should == 'root'
end
it 'should allow me to set gid as an array, breaking iptables' do
it 'should allow me to set gid as an array, and silently hide my error' do
@resource[:gid] = ['root', 'bobby']
@resource[:gid].should == ['root', 'bobby']
@resource[:gid].should == 'root'
end
end

describe ':set_mark' do
['1.3.2', '1.4.2'].each do |iptables_version|
describe "with iptables #{iptables_version}" do
before {
Facter.clear
Facter.fact(:iptables_version).stubs(:value).returns(iptables_version)
Facter.fact(:ip6tables_version).stubs(:value).returns(iptables_version)
}

if iptables_version == '1.3.2'
it 'should allow me to set set-mark without mask' do
@resource[:set_mark] = '0x3e8'
@resource[:set_mark].should == '0x3e8'
end
it 'should convert int to hex without mask' do
@resource[:set_mark] = '1000'
@resource[:set_mark].should == '0x3e8'
end
it 'should fail if mask is present' do
lambda { @resource[:set_mark] = '0x3e8/0xffffffff'}.should raise_error(
Puppet::Error, /iptables version #{iptables_version} does not support masks on MARK rules$/
)
end
end

if iptables_version == '1.4.2'
it 'should allow me to set set-mark with mask' do
@resource[:set_mark] = '0x3e8/0xffffffff'
@resource[:set_mark].should == '0x3e8/0xffffffff'
end
it 'should convert int to hex and add a 32 bit mask' do
@resource[:set_mark] = '1000'
@resource[:set_mark].should == '0x3e8/0xffffffff'
end
it 'should add a 32 bit mask' do
@resource[:set_mark] = '0x32'
@resource[:set_mark].should == '0x32/0xffffffff'
end
it 'should use the mask provided' do
@resource[:set_mark] = '0x32/0x4'
@resource[:set_mark].should == '0x32/0x4'
end
it 'should use the mask provided and convert int to hex' do
@resource[:set_mark] = '1000/0x4'
@resource[:set_mark].should == '0x3e8/0x4'
end
it 'should fail if mask value is more than 32 bits' do
lambda { @resource[:set_mark] = '1/4294967296'}.should raise_error(
Puppet::Error, /MARK mask must be integer or hex between 0 and 0xffffffff$/
)
end
it 'should fail if mask is malformed' do
lambda { @resource[:set_mark] = '1000/0xq4'}.should raise_error(
Puppet::Error, /MARK mask must be integer or hex between 0 and 0xffffffff$/
)
end
end

['/', '1000/', 'pwnie'].each do |bad_mark|
it "should fail with malformed mark '#{bad_mark}'" do
lambda { @resource[:set_mark] = bad_mark}.should raise_error(Puppet::Error)
end
end
it 'should fail if mark value is more than 32 bits' do
lambda { @resource[:set_mark] = '4294967296'}.should raise_error(
Puppet::Error, /MARK value must be integer or hex between 0 and 0xffffffff$/
)
end
end
end
end

[:chain, :jump].each do |param|
describe param do
it 'should autorequire fwchain when table and provider are undefined' do
@resource[param] = 'FOO'
@resource[:table].should == :filter
@resource[:provider].should == :iptables

chain = Puppet::Type.type(:firewallchain).new(:name => 'FOO:filter:IPv4')
catalog = Puppet::Resource::Catalog.new
catalog.add_resource @resource
catalog.add_resource chain
rel = @resource.autorequire[0]
rel.source.ref.should == chain.ref
rel.target.ref.should == @resource.ref
end

it 'should autorequire fwchain when table is undefined and provider is ip6tables' do
@resource[param] = 'FOO'
@resource[:table].should == :filter
@resource[:provider] = :ip6tables

chain = Puppet::Type.type(:firewallchain).new(:name => 'FOO:filter:IPv6')
catalog = Puppet::Resource::Catalog.new
catalog.add_resource @resource
catalog.add_resource chain
rel = @resource.autorequire[0]
rel.source.ref.should == chain.ref
rel.target.ref.should == @resource.ref
end

it 'should autorequire fwchain when table is raw and provider is undefined' do
@resource[param] = 'FOO'
@resource[:table] = :raw
@resource[:provider].should == :iptables

chain = Puppet::Type.type(:firewallchain).new(:name => 'FOO:raw:IPv4')
catalog = Puppet::Resource::Catalog.new
catalog.add_resource @resource
catalog.add_resource chain
rel = @resource.autorequire[0]
rel.source.ref.should == chain.ref
rel.target.ref.should == @resource.ref
end

it 'should autorequire fwchain when table is raw and provider is ip6tables' do
@resource[param] = 'FOO'
@resource[:table] = :raw
@resource[:provider] = :ip6tables

chain = Puppet::Type.type(:firewallchain).new(:name => 'FOO:raw:IPv6')
catalog = Puppet::Resource::Catalog.new
catalog.add_resource @resource
catalog.add_resource chain
rel = @resource.autorequire[0]
rel.source.ref.should == chain.ref
rel.target.ref.should == @resource.ref
end
end
end

describe ":chain and :jump" do
it 'should autorequire independent fwchains' do
@resource[:chain] = 'FOO'
@resource[:jump] = 'BAR'
@resource[:table].should == :filter
@resource[:provider].should == :iptables

chain_foo = Puppet::Type.type(:firewallchain).new(:name => 'FOO:filter:IPv4')
chain_bar = Puppet::Type.type(:firewallchain).new(:name => 'BAR:filter:IPv4')
catalog = Puppet::Resource::Catalog.new
catalog.add_resource @resource
catalog.add_resource chain_foo
catalog.add_resource chain_bar
rel = @resource.autorequire
rel[0].source.ref.should == chain_foo.ref
rel[0].target.ref.should == @resource.ref
rel[1].source.ref.should == chain_bar.ref
rel[1].target.ref.should == @resource.ref
end
end

describe ':pkttype' do
[:multicast, :broadcast, :unicast].each do |pkttype|
it "should accept pkttype value #{pkttype}" do
@resource[:pkttype] = pkttype
@resource[:pkttype].should == pkttype
end
end

it 'should fail when the pkttype value is not recognized' do
lambda { @resource[:pkttype] = 'not valid' }.should raise_error(Puppet::Error)
end
end
end
107 changes: 107 additions & 0 deletions spec/unit/puppet/type/firewallchain_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env rspec

require 'spec_helper'

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

describe firewallchain do
let(:klass) { firewallchain }
let(:provider) {
prov = stub 'provider'
prov.stubs(:name).returns(:iptables_chain)
prov
}
let(:resource) {
Puppet::Type::Firewallchain.stubs(:defaultprovider).returns provider
klass.new({:name => 'INPUT:filter:IPv4', :policy => :accept })
}

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

describe ':name' do
{'nat' => ['PREROUTING', 'POSTROUTING', 'OUTPUT'],
'mangle' => [ 'PREROUTING', 'POSTROUTING', 'INPUT', 'FORWARD', 'OUTPUT' ],
'filter' => ['INPUT','OUTPUT','FORWARD'],
'raw' => [ 'PREROUTING', 'OUTPUT'],
'broute' => ['BROUTING']
}.each_pair do |table, allowedinternalchains|
['IPv4', 'IPv6', 'ethernet'].each do |protocol|
[ 'test', '$5()*&%\'"^$09):' ].each do |chainname|
name = "#{chainname}:#{table}:#{protocol}"
if table == 'nat' && protocol == 'IPv6'
it "should fail #{name}" do
expect { resource[:name] = name }.to raise_error(Puppet::Error)
end
elsif protocol != 'ethernet' && table == 'broute'
it "should fail #{name}" do
expect { resource[:name] = name }.to raise_error(Puppet::Error)
end
else
it "should accept name #{name}" do
resource[:name] = name
resource[:name].should == name
end
end
end # chainname
end # protocol

[ 'PREROUTING', 'POSTROUTING', 'BROUTING', 'INPUT', 'FORWARD', 'OUTPUT' ].each do |internalchain|
name = internalchain + ':' + table + ':'
if internalchain == 'BROUTING'
name += 'ethernet'
elsif table == 'nat'
name += 'IPv4'
else
name += 'IPv4'
end
if allowedinternalchains.include? internalchain
it "should allow #{name}" do
resource[:name] = name
resource[:name].should == name
end
else
it "should fail #{name}" do
expect { resource[:name] = name }.to raise_error(Puppet::Error)
end
end
end # internalchain

end # table, allowedinternalchainnames

it 'should fail with invalid table names' do
expect { resource[:name] = 'wrongtablename:test:IPv4' }.to raise_error(Puppet::Error)
end

it 'should fail with invalid protocols names' do
expect { resource[:name] = 'test:filter:IPv5' }.to raise_error(Puppet::Error)
end

end

describe ':policy' do

[:accept, :drop, :queue, :return].each do |policy|
it "should accept policy #{policy}" do
resource[:policy] = policy
resource[:policy].should == policy
end
end

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

[:accept, :drop, :queue, :return].each do |policy|
it "non-inbuilt chains should not accept policy #{policy}" do
expect { klass.new({:name => 'testchain:filter:IPv4', :policy => policy }) }.to raise_error(Puppet::Error)
end
it "non-inbuilt chains can accept policies on protocol = ethernet (policy #{policy})" do
klass.new({:name => 'testchain:filter:ethernet', :policy => policy })
end
end

end

end
79 changes: 62 additions & 17 deletions spec/unit/puppet/util/firewall_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,78 @@

describe '#host_to_ip' do
subject { resource }
specify { subject.host_to_ip('puppetlabs.com').should == '96.126.112.51/32' }
specify {
Resolv.expects(:getaddress).with('puppetlabs.com').returns('96.126.112.51')
subject.host_to_ip('puppetlabs.com').should == '96.126.112.51/32'
}
specify { subject.host_to_ip('96.126.112.51').should == '96.126.112.51/32' }
specify { subject.host_to_ip('96.126.112.51/32').should == '96.126.112.51/32' }
specify { subject.host_to_ip('2001:db8:85a3:0:0:8a2e:370:7334').should == '2001:db8:85a3::8a2e:370:7334/128' }
specify { subject.host_to_ip('2001:db8:1234::/48').should == '2001:db8:1234::/48' }
specify { subject.host_to_ip('0.0.0.0/0').should == nil }
specify { subject.host_to_ip('::/0').should == nil }
end

describe '#icmp_name_to_number' do
subject { resource }
specify { subject.icmp_name_to_number('echo-reply').should == '0' }
specify { subject.icmp_name_to_number('destination-unreachable').should == '3' }
specify { subject.icmp_name_to_number('source-quench').should == '4' }
specify { subject.icmp_name_to_number('redirect').should == '6' }
specify { subject.icmp_name_to_number('echo-request').should == '8' }
specify { subject.icmp_name_to_number('router-advertisement').should == '9' }
specify { subject.icmp_name_to_number('router-solicitation').should == '10' }
specify { subject.icmp_name_to_number('time-exceeded').should == '11' }
specify { subject.icmp_name_to_number('parameter-problem').should == '12' }
specify { subject.icmp_name_to_number('timestamp-request').should == '13' }
specify { subject.icmp_name_to_number('timestamp-reply').should == '14' }
specify { subject.icmp_name_to_number('address-mask-request').should == '17' }
specify { subject.icmp_name_to_number('address-mask-reply').should == '18' }
describe 'proto unsupported' do
subject { resource }

%w{inet5 inet8 foo}.each do |proto|
it "should reject invalid proto #{proto}" do
expect { subject.icmp_name_to_number('echo-reply', proto) }.
to raise_error(ArgumentError, "unsupported protocol family '#{proto}'")
end
end
end

describe 'proto IPv4' do
proto = 'inet'
subject { resource }
specify { subject.icmp_name_to_number('echo-reply', proto).should == '0' }
specify { subject.icmp_name_to_number('destination-unreachable', proto).should == '3' }
specify { subject.icmp_name_to_number('source-quench', proto).should == '4' }
specify { subject.icmp_name_to_number('redirect', proto).should == '6' }
specify { subject.icmp_name_to_number('echo-request', proto).should == '8' }
specify { subject.icmp_name_to_number('router-advertisement', proto).should == '9' }
specify { subject.icmp_name_to_number('router-solicitation', proto).should == '10' }
specify { subject.icmp_name_to_number('time-exceeded', proto).should == '11' }
specify { subject.icmp_name_to_number('parameter-problem', proto).should == '12' }
specify { subject.icmp_name_to_number('timestamp-request', proto).should == '13' }
specify { subject.icmp_name_to_number('timestamp-reply', proto).should == '14' }
specify { subject.icmp_name_to_number('address-mask-request', proto).should == '17' }
specify { subject.icmp_name_to_number('address-mask-reply', proto).should == '18' }
end

describe 'proto IPv6' do
proto = 'inet6'
subject { resource }
specify { subject.icmp_name_to_number('destination-unreachable', proto).should == '1' }
specify { subject.icmp_name_to_number('time-exceeded', proto).should == '3' }
specify { subject.icmp_name_to_number('parameter-problem', proto).should == '4' }
specify { subject.icmp_name_to_number('echo-request', proto).should == '128' }
specify { subject.icmp_name_to_number('echo-reply', proto).should == '129' }
specify { subject.icmp_name_to_number('router-solicitation', proto).should == '133' }
specify { subject.icmp_name_to_number('router-advertisement', proto).should == '134' }
specify { subject.icmp_name_to_number('redirect', proto).should == '137' }
end
end

describe '#string_to_port' do
subject { resource }
specify { subject.string_to_port('80').should == '80' }
specify { subject.string_to_port('http').should == '80' }
specify { subject.string_to_port('80','tcp').should == '80' }
specify { subject.string_to_port(80,'tcp').should == '80' }
specify { subject.string_to_port('http','tcp').should == '80' }
specify { subject.string_to_port('domain','udp').should == '53' }
end

describe '#to_hex32' do
subject { resource }
specify { subject.to_hex32('0').should == '0x0' }
specify { subject.to_hex32('0x32').should == '0x32' }
specify { subject.to_hex32('42').should == '0x2a' }
specify { subject.to_hex32('4294967295').should == '0xffffffff' }
specify { subject.to_hex32('4294967296').should == nil }
specify { subject.to_hex32('-1').should == nil }
specify { subject.to_hex32('bananas').should == nil }
end
end
16 changes: 16 additions & 0 deletions spec/unit/puppet/util/ipcidr_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
specify { subject.netmask.should == '255.255.255.0' }
end

describe 'ipv4 open range with cidr' do
before { @ipcidr = Puppet::Util::IPCidr.new('0.0.0.0/0') }
subject { @ipcidr }
specify { subject.cidr.should == '0.0.0.0/0' }
specify { subject.prefixlen.should == 0 }
specify { subject.netmask.should == '0.0.0.0' }
end

describe 'ipv6 address' do
before { @ipaddr = Puppet::Util::IPCidr.new('2001:db8:85a3:0:0:8a2e:370:7334') }
subject { @ipaddr }
Expand All @@ -48,4 +56,12 @@
specify { subject.prefixlen.should == 48 }
specify { subject.netmask.should == 'ffff:ffff:ffff:0000:0000:0000:0000:0000' }
end

describe 'ipv6 open range with cidr' do
before { @ipaddr = Puppet::Util::IPCidr.new('::/0') }
subject { @ipaddr }
specify { subject.cidr.should == '::/0' }
specify { subject.prefixlen.should == 0 }
specify { subject.netmask.should == '0000:0000:0000:0000:0000:0000:0000:0000' }
end
end