From 47af7d0aadfe8c8501cbea8cc0467ff2a1ab4641 Mon Sep 17 00:00:00 2001 From: Matt Haught Date: Fri, 8 Nov 2019 01:14:27 -0500 Subject: [PATCH] add conntrack module support --- lib/puppet/provider/firewall/ip6tables.rb | 21 +- lib/puppet/provider/firewall/iptables.rb | 35 ++- lib/puppet/type/firewall.rb | 293 +++++++++++++++++++++- spec/unit/puppet/type/firewall_spec.rb | 102 ++++++++ 4 files changed, 442 insertions(+), 9 deletions(-) diff --git a/lib/puppet/provider/firewall/ip6tables.rb b/lib/puppet/provider/firewall/ip6tables.rb index eca66df..85f77df 100644 --- a/lib/puppet/provider/firewall/ip6tables.rb +++ b/lib/puppet/provider/firewall/ip6tables.rb @@ -3,6 +3,7 @@ Puppet::Type.type(:firewall).provide :ip6tables, parent: :iptables, source: :ip6 has_feature :iptables has_feature :connection_limiting + has_feature :conntrack has_feature :hop_limiting has_feature :rate_limiting has_feature :recent_limiting @@ -71,7 +72,19 @@ Puppet::Type.type(:firewall).provide :ip6tables, parent: :iptables, source: :ip6 connlimit_above: '-m connlimit --connlimit-above', connlimit_mask: '--connlimit-mask', connmark: '-m connmark --mark', - ctstate: '-m conntrack --ctstate', + ctstate: '--ctstate', + ctproto: '--ctproto', + ctorigsrc: '--ctorigsrc', + ctorigdst: '--ctorigdst', + ctreplsrc: '--ctreplsrc', + ctrepldst: '--ctrepldst', + ctorigsrcport: '--ctorigsrcport', + ctorigdstport: '--ctorigdstport', + ctreplsrcport: '--ctreplsrcport', + ctrepldstport: '--ctrepldstport', + ctstatus: '--ctstatus', + ctexpire: '--ctexpire', + ctdir: '--ctdir', destination: '-d', dport: ['-m multiport --dports', '--dport'], dst_range: '--dst-range', @@ -207,6 +220,8 @@ Puppet::Type.type(:firewall).provide :ip6tables, parent: :iptables, source: :ip6 addrtype: [:src_type, :dst_type], iprange: [:src_range, :dst_range], owner: [:uid, :gid], + conntrack: [:ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, + :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir], time: [:time_start, :time_stop, :month_days, :week_days, :date_start, :date_stop, :time_contiguous, :kernel_timezone], geoip: [:src_cc, :dst_cc], hashlimit: [:hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, :hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, @@ -253,7 +268,9 @@ Puppet::Type.type(:firewall).provide :ip6tables, parent: :iptables, source: :ip6 :proto, :ishasmorefrags, :islastfrag, :isfirstfrag, :src_range, :dst_range, :tcp_flags, :uid, :gid, :mac_source, :sport, :dport, :port, :src_type, :dst_type, :socket, :pkttype, :ipsec_dir, :ipsec_policy, :state, - :ctstate, :icmp, :hop_limit, :limit, :burst, :length, :recent, :rseconds, :reap, + :ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, + :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir, + :icmp, :hop_limit, :limit, :burst, :length, :recent, :rseconds, :reap, :rhitcount, :rttl, :rname, :mask, :rsource, :rdest, :ipset, :string, :string_algo, :string_from, :string_to, :jump, :clamp_mss_to_pmtu, :gateway, :todest, :tosource, :toports, :checksum_fill, :log_level, :log_prefix, :log_uid, :reject, :set_mss, :set_dscp, :set_dscp_class, :mss, :queue_num, :queue_bypass, diff --git a/lib/puppet/provider/firewall/iptables.rb b/lib/puppet/provider/firewall/iptables.rb index 3819060..30a1671 100644 --- a/lib/puppet/provider/firewall/iptables.rb +++ b/lib/puppet/provider/firewall/iptables.rb @@ -8,6 +8,7 @@ Puppet::Type.type(:firewall).provide :iptables, parent: Puppet::Provider::Firewa has_feature :iptables has_feature :connection_limiting + has_feature :conntrack has_feature :rate_limiting has_feature :recent_limiting has_feature :snat @@ -67,7 +68,19 @@ Puppet::Type.type(:firewall).provide :iptables, parent: Puppet::Provider::Firewa connlimit_above: '-m connlimit --connlimit-above', connlimit_mask: '--connlimit-mask', connmark: '-m connmark --mark', - ctstate: '-m conntrack --ctstate', + ctstate: '--ctstate', + ctproto: '--ctproto', + ctorigsrc: '--ctorigsrc', + ctorigdst: '--ctorigdst', + ctreplsrc: '--ctreplsrc', + ctrepldst: '--ctrepldst', + ctorigsrcport: '--ctorigsrcport', + ctorigdstport: '--ctorigdstport', + ctreplsrcport: '--ctreplsrcport', + ctrepldstport: '--ctrepldstport', + ctstatus: '--ctstatus', + ctexpire: '--ctexpire', + ctdir: '--ctdir', destination: '-d', dport: ['-m multiport --dports', '--dport'], dst_range: '--dst-range', @@ -214,6 +227,8 @@ Puppet::Type.type(:firewall).provide :iptables, parent: Puppet::Provider::Firewa addrtype: [:src_type, :dst_type], iprange: [:src_range, :dst_range], owner: [:uid, :gid], + conntrack: [:ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, + :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir], time: [:time_start, :time_stop, :month_days, :week_days, :date_start, :date_stop, :time_contiguous, :kernel_timezone], geoip: [:src_cc, :dst_cc], hashlimit: [:hashlimit_upto, :hashlimit_above, :hashlimit_name, :hashlimit_burst, :hashlimit_mode, :hashlimit_srcmask, :hashlimit_dstmask, @@ -296,7 +311,9 @@ Puppet::Type.type(:firewall).provide :iptables, parent: Puppet::Provider::Firewa :proto, :isfragment, :stat_mode, :stat_every, :stat_packet, :stat_probability, :src_range, :dst_range, :tcp_flags, :uid, :gid, :mac_source, :sport, :dport, :port, :src_type, :dst_type, :socket, :pkttype, :ipsec_dir, :ipsec_policy, - :state, :ctstate, :icmp, :limit, :burst, :length, :recent, :rseconds, :reap, + :state, :ctstate, :ctproto, :ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst, + :ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport, :ctstatus, :ctexpire, :ctdir, + :icmp, :limit, :burst, :length, :recent, :rseconds, :reap, :rhitcount, :rttl, :rname, :mask, :rsource, :rdest, :ipset, :string, :string_algo, :string_from, :string_to, :jump, :goto, :clusterip_new, :clusterip_hashmode, :clusterip_clustermac, :clusterip_total_nodes, :clusterip_local_node, :clusterip_hash_init, :queue_num, :queue_bypass, @@ -545,7 +562,7 @@ Puppet::Type.type(:firewall).provide :iptables, parent: Puppet::Provider::Firewa # POST PARSE CLUDGING ##################### - [:dport, :sport, :port, :state, :ctstate].each do |prop| + [:dport, :sport, :port, :state, :ctstate, :ctstatus].each do |prop| hash[prop] = hash[prop].split(',') unless hash[prop].nil? end @@ -607,6 +624,17 @@ Puppet::Type.type(:firewall).provide :iptables, parent: Puppet::Provider::Firewa [ :connmark, :ctstate, + :ctproto, + :ctorigsrc, + :ctorigdst, + :ctreplsrc, + :ctrepldst, + :ctorigsrcport, + :ctorigdstport, + :ctreplsrcport, + :ctrepldstport, + :ctstatus, + :ctexpire, :destination, :dport, :dst_range, @@ -646,6 +674,7 @@ Puppet::Type.type(:firewall).provide :iptables, parent: Puppet::Provider::Firewa # iptables-save and user supplied resources is consistent. hash[:state] = hash[:state].sort unless hash[:state].nil? hash[:ctstate] = hash[:ctstate].sort unless hash[:ctstate].nil? + hash[:ctstatus] = hash[:ctstatus].sort unless hash[:ctstatus].nil? # This forces all existing, commentless rules or rules with invalid comments to be moved # to the bottom of the stack. diff --git a/lib/puppet/type/firewall.rb b/lib/puppet/type/firewall.rb index cf27165..9beb4a7 100644 --- a/lib/puppet/type/firewall.rb +++ b/lib/puppet/type/firewall.rb @@ -33,7 +33,7 @@ Puppet::Type.newtype(:firewall) do * ip6tables: Ip6tables type provider * Required binaries: ip6tables-save, ip6tables. - * Supported features: address_type, connection_limiting, dnat, hop_limiting, icmp_match, + * Supported features: address_type, connection_limiting, conntrack, dnat, hop_limiting, icmp_match, interface_match, iprange, ipsec_dir, ipsec_policy, ipset, iptables, isfirstfrag, ishasmorefrags, islastfrag, length, log_level, log_prefix, log_uid, mark, mask, mss, owner, pkttype, queue_bypass, queue_num, rate_limiting, recent_limiting, reject_type, @@ -43,7 +43,7 @@ Puppet::Type.newtype(:firewall) do * Required binaries: iptables-save, iptables. * Default for kernel == linux. - * Supported features: address_type, clusterip, connection_limiting, dnat, icmp_match, + * Supported features: address_type, clusterip, connection_limiting, conntrack, dnat, icmp_match, interface_match, iprange, ipsec_dir, ipsec_policy, ipset, iptables, isfragment, length, log_level, log_prefix, log_uid, mark, mask, mss, netmap, nflog_group, nflog_prefix, nflog_range, nflog_threshold, owner, pkttype, queue_bypass, queue_num, rate_limiting, @@ -56,6 +56,8 @@ Puppet::Type.newtype(:firewall) do * connection_limiting: Connection limiting features. + * conntrack: Connection tracking features. + * dnat: Destination NATing. * hop_limiting: Hop limiting features. @@ -134,6 +136,7 @@ Puppet::Type.newtype(:firewall) do PUPPETCODE feature :connection_limiting, 'Connection limiting features.' + feature :conntrack, 'Connection tracking features.' feature :hop_limiting, 'Hop limiting features.' feature :rate_limiting, 'Rate limiting features.' feature :recent_limiting, 'The netfilter recent module' @@ -910,7 +913,7 @@ Puppet::Type.newtype(:firewall) do end end - newproperty(:ctstate, array_matching: :all, required_features: :state_match) do + newproperty(:ctstate, array_matching: :all, required_features: :conntrack) do desc <<-PUPPETCODE Matches a packet based on its state in the firewall stateful inspection table, using the conntrack module. Values can be: @@ -920,9 +923,11 @@ Puppet::Type.newtype(:firewall) do * NEW * RELATED * UNTRACKED + * SNAT + * DNAT PUPPETCODE - newvalues(:INVALID, :ESTABLISHED, :NEW, :RELATED, :UNTRACKED) + newvalues(:INVALID, :ESTABLISHED, :NEW, :RELATED, :UNTRACKED, :SNAT, :DNAT) # States should always be sorted. This normalizes the resource states to # keep it consistent with the sorted result from iptables-save. @@ -940,6 +945,286 @@ Puppet::Type.newtype(:firewall) do end end + newproperty(:ctproto, required_features: :conntrack) do + desc <<-PUPPETCODE + The specific layer-4 protocol number to match for this rule using the + conntrack module. + PUPPETCODE + newvalue(%r{^!?\s?\d+$}) + end + + newproperty(:ctorigsrc, required_features: :conntrack) do + desc <<-PUPPETCODE + The original source address using the conntrack module. For example: + + ctorigsrc => '192.168.2.0/24' + + You can also negate a mask by putting ! in front. For example: + + ctorigsrc => '! 192.168.2.0/24' + + The ctorigsrc can also be an IPv6 address if your provider supports it. + PUPPETCODE + + munge do |value| + case @resource[:provider] + when :iptables + protocol = :IPv4 + when :ip6tables + protocol = :IPv6 + else + raise('cannot work out protocol family') + end + + begin + @resource.host_to_mask(value, protocol) + if protocol == :IPv4 + value.chomp("/32") + elsif protocol == :IPv6 + value.chomp("/128") + end + rescue StandardError => e + raise("host_to_ip failed for #{value}, exception #{e}") + end + end + end + + newproperty(:ctorigdst, required_features: :conntrack) do + desc <<-PUPPETCODE + The original destination address using the conntrack module. For example: + + ctorigdst => '192.168.2.0/24' + + You can also negate a mask by putting ! in front. For example: + + ctorigdst => '! 192.168.2.0/24' + + The ctorigdst can also be an IPv6 address if your provider supports it. + PUPPETCODE + + munge do |value| + case @resource[:provider] + when :iptables + protocol = :IPv4 + when :ip6tables + protocol = :IPv6 + else + raise('cannot work out protocol family') + end + + begin + @resource.host_to_mask(value, protocol) + if protocol == :IPv4 + value.chomp("/32") + elsif protocol == :IPv6 + value.chomp("/128") + end + rescue StandardError => e + raise("host_to_ip failed for #{value}, exception #{e}") + end + end + end + + newproperty(:ctreplsrc, required_features: :conntrack) do + desc <<-PUPPETCODE + The reply source address using the conntrack module. For example: + + ctreplsrc => '192.168.2.0/24' + + You can also negate a mask by putting ! in front. For example: + + ctreplsrc => '! 192.168.2.0/24' + + The ctreplsrc can also be an IPv6 address if your provider supports it. + PUPPETCODE + + munge do |value| + case @resource[:provider] + when :iptables + protocol = :IPv4 + when :ip6tables + protocol = :IPv6 + else + raise('cannot work out protocol family') + end + + begin + @resource.host_to_mask(value, protocol) + if protocol == :IPv4 + value.chomp("/32") + elsif protocol == :IPv6 + value.chomp("/128") + end + rescue StandardError => e + raise("host_to_ip failed for #{value}, exception #{e}") + end + end + end + + newproperty(:ctrepldst, required_features: :conntrack) do + desc <<-PUPPETCODE + The reply destination address using the conntrack module. For example: + + ctrepldst => '192.168.2.0/24' + + You can also negate a mask by putting ! in front. For example: + + ctrepldst => '! 192.168.2.0/24' + + The ctrepldst can also be an IPv6 address if your provider supports it. + PUPPETCODE + + munge do |value| + case @resource[:provider] + when :iptables + protocol = :IPv4 + when :ip6tables + protocol = :IPv6 + else + raise('cannot work out protocol family') + end + + begin + @resource.host_to_mask(value, protocol) + if protocol == :IPv4 + value.chomp("/32") + elsif protocol == :IPv6 + value.chomp("/128") + end + rescue StandardError => e + raise("host_to_ip failed for #{value}, exception #{e}") + end + end + end + + newproperty(:ctorigsrcport, required_features: :conntrack) do + desc <<-PUPPETCODE + The original source port to match for this filter using the conntrack module. + For example: + + ctorigsrcport => '80' + + You can also specify a port range: For example: + + ctorigsrcport => '80:81' + + You can also negate a port by putting ! in front. For example: + + ctorigsrcport => '! 80' + + PUPPETCODE + newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) + end + + newproperty(:ctorigdstport, required_features: :conntrack) do + desc <<-PUPPETCODE + The original destination port to match for this filter using the conntrack module. + For example: + + ctorigdstport => '80' + + You can also specify a port range: For example: + + ctorigdstport => '80:81' + + You can also negate a port by putting ! in front. For example: + + ctorigdstport => '! 80' + + PUPPETCODE + newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) + end + + newproperty(:ctreplsrcport, required_features: :conntrack) do + desc <<-PUPPETCODE + The reply source port to match for this filter using the conntrack module. + For example: + + ctreplsrcport => '80' + + You can also specify a port range: For example: + + ctreplsrcport => '80:81' + + You can also negate a port by putting ! in front. For example: + + ctreplsrcport => '! 80' + + PUPPETCODE + newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) + end + + newproperty(:ctrepldstport, required_features: :conntrack) do + desc <<-PUPPETCODE + The reply destination port to match for this filter using the conntrack module. + For example: + + ctrepldstport => '80' + + You can also specify a port range: For example: + + ctrepldstport => '80:81' + + You can also negate a port by putting ! in front. For example: + + ctrepldstport => '! 80' + + PUPPETCODE + newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) + end + + newproperty(:ctstatus, array_matching: :all, required_features: :conntrack) do + desc <<-PUPPETCODE + Matches a packet based on its status using the conntrack module. Values can be: + + * EXPECTED + * SEEN_REPLY + * ASSURED + * CONFIRMED + PUPPETCODE + + newvalues(:NONE, :EXPECTED, :SEEN_REPLY, :ASSURED, :CONFIRMED) + + # Statuses should always be sorted. This normalizes the resource status to + # keep it consistent with the sorted result from iptables-save. + def should=(values) + @should = super(values).sort_by { |sym| sym.to_s } + end + + def to_s?(value) + should_to_s(value) + end + + def should_to_s(value) + value = [value] unless value.is_a?(Array) + value.join(',') + end + end + + newproperty(:ctexpire, required_features: :conntrack) do + desc <<-PUPPETCODE + Matches a packet based on lifetime remaining in seconds or range of values + using the conntrack module. For example: + + ctexpire => '100:150' + + PUPPETCODE + newvalue(%r{^!?\s?\d+$|^!?\s?\d+\:\d+$}) + end + + newproperty(:ctdir, required_features: :conntrack) do + desc <<-PUPPETCODE + Matches a packet that is flowing in the specified direction using the + conntrack module. If this flag is not specified at all, matches packets + in both directions. Values can be: + + * REPLY + * ORIGINAL + PUPPETCODE + + newvalues(:REPLY, :ORIGINAL) + end + # Connection mark newproperty(:connmark, required_features: :mark) do desc <<-PUPPETCODE diff --git a/spec/unit/puppet/type/firewall_spec.rb b/spec/unit/puppet/type/firewall_spec.rb index 6f29602..8897b32 100755 --- a/spec/unit/puppet/type/firewall_spec.rb +++ b/spec/unit/puppet/type/firewall_spec.rb @@ -436,6 +436,108 @@ describe firewall do # rubocop:disable RSpec/MultipleDescribes end end + describe ':ctproto' do + it 'accepts numeric value' do + resource[:ctproto] = 6 + expect(resource[:ctproto]).to eql 6 + end + it 'accepts negated string value' do + resource[:ctproto] = '! 6' + expect(resource[:ctproto]).to eql '! 6' + end + end + + [:ctorigsrc, :ctorigdst, :ctreplsrc, :ctrepldst].each do |addr| + describe addr do + it "should accept a #{addr} as a string without /32" do + resource[addr] = '127.0.0.1' + expect(resource[addr]).to eql '127.0.0.1' + end + it "should accept a #{addr} as a string with /32" do + resource[addr] = '127.0.0.1/32' + expect(resource[addr]).to eql '127.0.0.1' + end + it "should accept a #{addr} as a string with cidr" do + resource[addr] = '10.0.0.0/8' + expect(resource[addr]).to eql '10.0.0.0/8' + end + it "should accept a #{addr} as a string with ipv6 cidr" do + resource[addr] = '2001:DB8::/64' + expect(resource[addr]).to eql '2001:DB8::/64' + end + it "should accept a negated #{addr} as a string" do + resource[addr] = '! 127.0.0.1' + expect(resource[addr]).to eql '! 127.0.0.1' + end + it "should accept a negated #{addr} as a string with cidr" do + resource[addr] = '! 10.0.0.0/8' + expect(resource[addr]).to eql '! 10.0.0.0/8' + end + end + end + + [:ctorigsrcport, :ctorigdstport, :ctreplsrcport, :ctrepldstport].each do |port| + describe port do + it "should accept #{port} as numeric value" do + resource[port] = 80 + expect(resource[port]).to eql 80 + end + it "should accept #{port} as range value" do + resource[port] = '80:81' + expect(resource[port]).to eql '80:81' + end + it "should accept a negated #{port} as string value" do + resource[port] = '! 80' + expect(resource[port]).to eql '! 80' + end + it "should accept a negated #{port} as range value" do + resource[port] = '! 80:81' + expect(resource[port]).to eql '! 80:81' + end + end + end + + describe ':ctstatus' do + it 'accepts value as a string - EXPECTED' do + resource[:ctstatus] = :EXPECTED + expect(resource[:ctstatus]).to eql [:EXPECTED] + end + + it 'accepts value as an array - EXPECTED, SEEN_REPLY' do + resource[:ctstatus] = [:EXPECTED, :SEEN_REPLY] + expect(resource[:ctstatus]).to eql [:EXPECTED, :SEEN_REPLY] + end + + it 'sorts values alphabetically - SEEN_REPLY, EXPECTED' do + resource[:ctstatus] = [:SEEN_REPLY, :EXPECTED] + expect(resource[:ctstatus]).to eql [:EXPECTED, :SEEN_REPLY] + end + end + + describe ':ctexpire' do + it 'accepts numeric values' do + resource[:ctexpire] = 100 + expect(resource[:ctexpire]).to eql 100 + end + + it 'accepts numeric range values' do + resource[:ctexpire] = '100:120' + expect(resource[:ctexpire]).to eql '100:120' + end + end + + describe ':ctdir' do + it 'accepts value as a string - REPLY' do + resource[:ctdir] = :REPLY + expect(resource[:ctdir]).to be :REPLY + end + + it 'accepts value as a string - ORIGINAL' do + resource[:ctdir] = :ORIGINAL + expect(resource[:ctdir]).to be :ORIGINAL + end + end + describe ':burst' do it 'accepts numeric values' do resource[:burst] = 12 -- 2.45.2