Skip to content

Commit

Permalink
Merge pull request #208 from georgkoester/hop_limit2
Browse files Browse the repository at this point in the history
Add support for IPv6 hop limiting
  • Loading branch information
Ashley Penney committed Sep 13, 2013
2 parents 82cc5ff + c64d379 commit 8221892
Show file tree
Hide file tree
Showing 5 changed files with 252 additions and 12 deletions.
20 changes: 18 additions & 2 deletions lib/puppet/provider/firewall/ip6tables.rb
Expand Up @@ -2,6 +2,7 @@
@doc = "Ip6tables type provider"

has_feature :iptables
has_feature :hop_limiting
has_feature :rate_limiting
has_feature :snat
has_feature :dnat
Expand All @@ -15,6 +16,9 @@
has_feature :mark
has_feature :tcp_flags
has_feature :pkttype
has_feature :ishasmorefrags
has_feature :islastfrag
has_feature :isfirstfrag

optional_commands({
:ip6tables => 'ip6tables',
Expand All @@ -39,6 +43,7 @@ def self.iptables_save(*args)
:icmp => "-m icmp6 --icmpv6-type",
:iniface => "-i",
:jump => "-j",
:hop_limit => "-m hl --hl-eq",
:limit => "-m limit --limit",
:log_level => "--log-level",
:log_prefix => "--log-prefix",
Expand All @@ -55,7 +60,10 @@ def self.iptables_save(*args)
:toports => "--to-ports",
:tosource => "--to-source",
:uid => "-m owner --uid-owner",
:pkttype => "-m pkttype --pkt-type"
:pkttype => "-m pkttype --pkt-type",
:ishasmorefrags => "-m frag --fragid 0 --fragmore",
:islastfrag => "-m frag --fragid 0 --fraglast",
:isfirstfrag => "-m frag --fragid 0 --fragfirst",
}

# Create property methods dynamically
Expand All @@ -73,8 +81,16 @@ def self.iptables_save(*args)
# 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
# (Note: on my CentOS 6.4 ip6tables-save returns -m frag on the place
# I put it when calling the command. So compability with manual changes
# not provided with current parser [georg.koester])
@resource_list = [:table, :source, :destination, :iniface, :outiface,
:proto, :gid, :uid, :sport, :dport, :port, :pkttype, :name, :state, :icmp, :limit, :burst, :jump,
:proto, :ishasmorefrags, :islastfrag, :isfirstfrag, :gid, :uid, :sport, :dport,
:port, :pkttype, :name, :state, :icmp, :hop_limit, :limit, :burst, :jump,
:todest, :tosource, :toports, :log_level, :log_prefix, :reject]

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

end
23 changes: 13 additions & 10 deletions lib/puppet/provider/firewall/iptables.rb
Expand Up @@ -76,6 +76,11 @@
:isfragment => "-f",
}

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


# Create property methods dynamically
(@resource_map.keys << :chain << :table << :action).each do |property|
define_method "#{property}" do
Expand Down Expand Up @@ -154,10 +159,6 @@ def self.rule_to_hash(line, table, counter)
keys = []
values = line.dup

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

####################
# PRE-PARSE CLUDGING
####################
Expand All @@ -167,14 +168,15 @@ def self.rule_to_hash(line, table, counter)
values = values.sub(/--tcp-flags (\S*) (\S*)/, '--tcp-flags "\1 \2"')

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

Expand Down Expand Up @@ -214,7 +216,7 @@ def self.rule_to_hash(line, table, counter)
end

# Convert booleans removing the previous cludge we did
known_booleans.each do |bool|
@known_booleans.each do |bool|
if hash[bool] != nil then
unless hash[bool] == "true" then
raise "Parser error: #{bool} was meant to be a boolean but received value: #{hash[bool]}."
Expand Down Expand Up @@ -309,13 +311,14 @@ def general_args
args = []
resource_list = self.class.instance_variable_get('@resource_list')
resource_map = self.class.instance_variable_get('@resource_map')
known_booleans = self.class.instance_variable_get('@known_booleans')

resource_list.each do |res|
resource_value = nil
if (resource[res]) then
resource_value = resource[res]
# If socket is true then do not add the value as -m socket is standalone
if res == :socket then
if known_booleans.include?(res) then
resource_value = nil
end
if res == :isfragment then
Expand Down
37 changes: 37 additions & 0 deletions lib/puppet/type/firewall.rb
Expand Up @@ -28,6 +28,7 @@
installed.
EOS

feature :hop_limiting, "Hop limiting features."
feature :rate_limiting, "Rate limiting features."
feature :snat, "Source NATing"
feature :dnat, "Destination NATing"
Expand All @@ -45,6 +46,9 @@
feature :isfragment, "Match fragments"
feature :address_type, "The ability match on source or destination address type"
feature :iprange, "The ability match on source or destination IP range "
feature :ishasmorefrags, "Match a non-last fragment of a fragmented ipv6 packet - might be first"
feature :islastfrag, "Match the last fragment of an ipv6 packet"
feature :isfirstfrag, "Match the first fragment of a fragmented ipv6 packet"

# provider specific features
feature :iptables, "The provider provides iptables features."
Expand Down Expand Up @@ -549,6 +553,14 @@ def should_to_s(value)
end
end

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

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

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

newvalues(:true, :false)
end

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

newvalues(:true, :false)
end

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

newvalues(:true, :false)
end

newparam(:line) do
desc <<-EOS
Read-only property for caching the rule line.
Expand Down
107 changes: 107 additions & 0 deletions spec/fixtures/ip6tables/conversion_hash.rb
@@ -0,0 +1,107 @@
# These hashes allow us to iterate across a series of test data
# creating rspec examples for each parameter to ensure the input :line
# extrapolates to the desired value for the parameter in question. And
# vice-versa

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

# This hash is for testing converting a hash to an argument line.
HASH_TO_ARGS6 = {
'zero_prefixlen_ipv6' => {
:params => {
:name => '100 zero prefix length ipv6',
:table => 'filter',
:provider => 'ip6tables',
:source => '::/0',
:destination => '::/0',
},
:args => ['-t', :filter, '-p', :tcp, '-m', 'comment', '--comment', '100 zero prefix length ipv6'],
},
'source_destination_ipv4_no_cidr' => {
:params => {
:name => '000 source destination ipv4 no cidr',
:table => 'filter',
:provider => 'ip6tables',
:source => '1.1.1.1',
:destination => '2.2.2.2',
},
:args => ['-t', :filter, '-s', '1.1.1.1/32', '-d', '2.2.2.2/32', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv4 no cidr'],
},
'source_destination_ipv6_no_cidr' => {
:params => {
:name => '000 source destination ipv6 no cidr',
:table => 'filter',
:provider => 'ip6tables',
:source => '2001:db8:1234::',
:destination => '2001:db8:4321::',
},
:args => ['-t', :filter, '-s', '2001:db8:1234::/128', '-d', '2001:db8:4321::/128', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv6 no cidr'],
},
'source_destination_ipv6_netmask' => {
:params => {
:name => '000 source destination ipv6 netmask',
:table => 'filter',
:provider => 'ip6tables',
:source => '2001:db8:1234::/ffff:ffff:ffff:0000:0000:0000:0000:0000',
:destination => '2001:db8:4321::/ffff:ffff:ffff:0000:0000:0000:0000:0000',
},
:args => ['-t', :filter, '-s', '2001:db8:1234::/48', '-d', '2001:db8:4321::/48', '-p', :tcp, '-m', 'comment', '--comment', '000 source destination ipv6 netmask'],
},
'frag_ishasmorefrags' => {
:params => {
:name => "100 has more fragments",
:ishasmorefrags => true,
:provider => 'ip6tables',
:table => "filter",
},
:args => ["-t", :filter, "-p", :tcp, "-m", "frag", "--fragid", "0", "--fragmore", "-m", "comment", "--comment", "100 has more fragments"],
},
'frag_islastfrag' => {
:params => {
:name => "100 last fragment",
:islastfrag => true,
:provider => 'ip6tables',
:table => "filter",
},
:args => ["-t", :filter, "-p", :tcp, "-m", "frag", "--fragid", "0", "--fraglast", "-m", "comment", "--comment", "100 last fragment"],
},
'frag_isfirstfrags' => {
:params => {
:name => "100 first fragment",
:isfirstfrag => true,
:provider => 'ip6tables',
:table => "filter",
},
:args => ["-t", :filter, "-p", :tcp, "-m", "frag", "--fragid", "0", "--fragfirst", "-m", "comment", "--comment", "100 first fragment"],
},
'hop_limit' => {
:params => {
:name => "100 hop limit",
:hop_limit => 255,
:provider => 'ip6tables',
:table => "filter",
},
:args => ["-t", :filter, "-p", :tcp, "-m", "comment", "--comment", "100 hop limit", "-m", "hl", "--hl-eq", 255],
},
}
77 changes: 77 additions & 0 deletions spec/unit/puppet/provider/iptables_spec.rb
Expand Up @@ -162,3 +162,80 @@
end
end
end

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

before :each do
Puppet::Type::Firewall.stubs(:ip6tables).returns provider6
provider6.stubs(:command).with(:ip6tables_save).returns "/sbin/ip6tables-save"

# Stub iptables version
Facter.fact(:ip6tables_version).stubs(:value).returns("1.4.7")

Puppet::Util::Execution.stubs(:execute).returns ""
Puppet::Util.stubs(:which).with("ip6tables-save").
returns "/sbin/ip6tables-save"
end

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

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

provider6.instances.length.should == 0
end

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

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

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

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

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

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

0 comments on commit 8221892

Please sign in to comment.