File: firewallchain.rb

package info (click to toggle)
puppet-module-puppetlabs-firewall 8.1.7-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 512 kB
  • sloc: ruby: 2,674; sh: 39; makefile: 2
file content (235 lines) | stat: -rw-r--r-- 11,190 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# frozen_string_literal: true

require_relative '../../../puppet_x/puppetlabs/firewall/utility'

# Implementation for the firewallchain type using the Resource API.
class Puppet::Provider::Firewallchain::Firewallchain
  ###### GLOBAL VARIABLES ######

  # Command to list all chains and rules
  $list_command = {
    'IPv4' => 'iptables-save',
    'iptables' => 'iptables-save',
    'IPv6' => 'ip6tables-save',
    'ip6tables' => 'ip6tables-save'
  }
  # Regex used to divide output of$list_command between tables
  $table_regex = %r{(\*(?:nat|mangle|filter|raw|rawpost|broute|security)[^*]+)}
  # Array of all the supported iptables
  $supported_tables = ['nat', 'mangle', 'filter', 'raw', 'rawpost', 'broute', 'security']
  # Regex used to retrieve Chains
  $chain_regex = %r{\n:(INPUT|FORWARD|OUTPUT|(?:\S+))(?:\s(ACCEPT|DROP|QEUE|RETURN|PREROUTING|POSTROUTING))?}
  # Base commands for the protocols, including table affixes
  $base_command = {
    'IPv4' => 'iptables -t',
    'iptables' => 'iptables -t',
    'IPv6' => 'ip6tables -t',
    'ip6tables' => 'ip6tables -t',
  }
  # Command to create a chain
  $chain_create_command = '-N'
  # Command to flush all rules from a chain, must be used before deleting
  $chain_flush_command = '-F'
  # Command to delete a chain, cannot be used on inbuilt
  $chain_delete_command = '-X'
  # Command to set chain policy, works on inbuilt chains only
  $chain_policy_command = '-P'
  # Command to list specific table so it will generate necessary output for iptables-save
  # The retrieval of in-built chains may get confused by `iptables-save` tendency to not return table information
  # for tables that have not yet been interacted with.
  $table_list_command = '-L'
  # Check if the given chain name references a built in one
  $built_in_regex = %r{^(?:INPUT|OUTPUT|FORWARD|PREROUTING|POSTROUTING)$}

  ###### PUBLIC METHODS ######

  # Raw data is retrieved via `iptables-save` and then regexed to retrieve the different Chains and whether they have a set Policy
  def get(_context)
    # Create empty return array
    chains = []
    # Scan String to retrieve all Chains and Policies
    ['IPv4', 'IPv6'].each do |protocol|
      # Go through each supported table and retrieve its chains if it exists.
      $supported_tables.each do |table_name|
        cmd_output = Puppet::Provider.execute([$list_command[protocol], '-t', table_name].join(' '), failonfail: false)
        cmd_output.scan($chain_regex).each do |chain|
          # Create the base hash
          chain_hash = {
            name: "#{chain[0]}:#{table_name}:#{protocol}",
            purge: false,
            ignore_foreign: false,
            ensure: 'present'
          }
          # If a policy was found add to the hash
          chain_hash[:policy] = chain[1].downcase if chain[1]
          chains << chain_hash
        end
      end
    end
    # Return array
    chains
  end

  def set(context, changes)
    changes.each do |name, change|
      is = change[:is]
      should = change[:should]

      is = PuppetX::Firewall::Utility.create_absent(:name, name) if is.nil?
      should = PuppetX::Firewall::Utility.create_absent(:name, name) if should.nil?

      # Process the input and divide the name into it's relevant parts
      is, should = Puppet::Provider::Firewallchain::Firewallchain.process_input(is, should)
      # Run static verification against both sets of values
      Puppet::Provider::Firewallchain::Firewallchain.verify(is, should)

      if is[:ensure].to_s == 'absent' && should[:ensure].to_s == 'present'
        context.creating(name) do
          create(context, name, should)
        end
      elsif is[:ensure].to_s == 'present' && should[:ensure].to_s == 'absent'
        context.deleting(name) do
          delete(context, name, is)
        end
      elsif is[:ensure].to_s == 'present' && should[:ensure].to_s == 'present'
        context.updating(name) do
          update(context, name, should, is)
        end
      end
    end
  end

  def create(context, name, should)
    context.notice("Creating Chain '#{name}' with #{should.inspect}")
    # If a built-in chain is not present we assume that corresponding table has not been interacted with
    if $built_in_regex.match(should[:chain])
      Puppet::Provider.execute([$base_command[should[:protocol]], should[:table], $table_list_command].join(' '))
    else
      Puppet::Provider.execute([$base_command[should[:protocol]], should[:table], $chain_create_command, should[:chain]].join(' '))
    end
    PuppetX::Firewall::Utility.persist_iptables(context, name, should[:protocol])
  end

  def update(context, name, should, is)
    # Skip the update if not a inbuilt chain or if policy has not been updated
    return if !$built_in_regex.match(should[:chain]) ||
              ($built_in_regex.match(should[:chain]) && is[:policy] == should[:policy])

    context.notice("Updating Chain '#{name}' with #{should.inspect}")
    Puppet::Provider.execute([$base_command[should[:protocol]], should[:table], $chain_policy_command, should[:chain], should[:policy].upcase].join(' '))
    PuppetX::Firewall::Utility.persist_iptables(context, name, should[:protocol])
  end

  def delete(context, name, is)
    # Before we can delete a chain we must first flush it of any active rules
    context.notice("Flushing Chain '#{name}'")
    Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], $chain_flush_command, is[:chain]].join(' '))

    # For Inbuilt chains we cannot delete them and so instead simply ensure they are reverted to the default policy
    if $built_in_regex.match(is[:chain])
      context.notice("Reverting Internal Chain '#{name}' to its default")
      Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], $chain_policy_command, is[:chain], 'ACCEPT'].join(' '))
    else
      context.notice("Deleting Chain '#{name}'")
      Puppet::Provider.execute([$base_command[is[:protocol]], is[:table], $chain_delete_command, is[:chain]].join(' '))
    end
    PuppetX::Firewall::Utility.persist_iptables(context, name, is[:protocol])
  end

  # Custom insync method
  def insync?(context, _name, property_name, _is_hash, _should_hash)
    context.debug("Checking whether '#{property_name}' is out of sync")

    case property_name
    when :purge, :ignore, :ignore_foreign
      # Suppres any update notifications for the purge/ignore(_foreign) values
      # They are used in the generate method which is ran prior to this point and have no
      # bearing on it's actual state.
      true
    else
      nil
    end
  end

  ###### PRIVATE METHODS ######

  # Process the information so that it can be correctly applied
  # @api private
  def self.process_input(is, should)
    # Split the name into it's relevant parts
    is[:name] = is[:title] if is[:name].nil?
    is[:chain], is[:table], is[:protocol] = is[:name].split(':')
    should[:name] = should[:title] if should[:name].nil?
    should[:chain], should[:table], should[:protocol] = should[:name].split(':')

    # If an in-built chain, ensure it is assigned a policy
    is[:policy] = 'accept' if $built_in_regex.match(is[:chain]) && is[:policy].nil?
    # For the same reason assign it the default policy as an intended state if it does not have one
    should[:policy] = 'accept' if $built_in_regex.match(should[:chain]) && should[:policy].nil?

    [is, should]
  end

  # Verify that the information is correct
  # @api private
  def self.verify(_is, should)
    # Verify that no incorrect chain names are passed
    case should[:table]
    when 'filter'
      raise ArgumentError, 'INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table \'filter\'' if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(should[:chain])
    when 'mangle'
      raise ArgumentError, 'PREROUTING, POSTROUTING, INPUT, FORWARD and OUTPUT are the only inbuilt chains that can be used in table \'mangle\'' if %r{^(BROUTING)$}.match?(should[:chain])
    when 'nat'
      raise ArgumentError, 'PREROUTING, POSTROUTING, INPUT, and OUTPUT are the only inbuilt chains that can be used in table \'nat\'' if %r{^(BROUTING|FORWARD)$}.match?(should[:chain])
    when 'raw'
      raise ArgumentError, 'PREROUTING and OUTPUT are the only inbuilt chains in the table \'raw\'' if %r{^(POSTROUTING|BROUTING|INPUT|FORWARD)$}.match?(should[:chain])
    when 'broute'
      raise ArgumentError, 'BROUTE is only valid with protocol \'ethernet\'' if should[:protocol] != 'ethernet'
      raise ArgumentError, 'BROUTING is the only inbuilt chain allowed on on table \'broute\'' if %r{^PREROUTING|POSTROUTING|INPUT|FORWARD|OUTPUT$}.match?(should[:chain])
    when 'security'
      raise ArgumentError, 'INPUT, OUTPUT and FORWARD are the only inbuilt chains that can be used in table \'security\'' if %r{^(PREROUTING|POSTROUTING|BROUTING)$}.match?(should[:chain])
    end

    # Verify that Policy is only passed for the inbuilt chains
    raise ArgumentError, "'policy' can only be set on Internal Chains. Setting for '#{should[:name]}' is invalid" if !$built_in_regex.match(should[:chain]) && should.key?(:policy)

    # Warn that inbuilt chains will be flushed, not deleted
    warn "Warning: Inbuilt Chains may not be deleted. Chain `#{should[:name]}` will be flushed and have it's policy reverted to default." if $built_in_regex.match(should[:chain]) &&
                                                                                                                                             should[:ensure] == 'absent'
  end

  # Customer generate method called by the resource_api
  # Finds and returns all unmanaged rules on the chain that are not set to be ignored
  def generate(_context, title, _is, should)
    # Unless purge is true, return an empty array
    return [] unless should[:purge]

    # gather a list of all rules present on the system
    rules_resources = Puppet::Type.type(:firewall).instances

    # Retrieve information from the title
    name, table, protocol = title.split(':')

    # Keep only rules in this chain
    rules_resources.delete_if do |resource|
      resource.rsapi_current_state[:chain] != name || resource.rsapi_current_state[:table] != table || resource.rsapi_current_state[:protocol] != protocol
    end

    # Remove rules which match our ignore filter
    # Ensure ignore value is wrapped as an array to simplify the code
    should[:ignore] = [should[:ignore]] if should[:ignore].is_a?(String)
    rules_resources.delete_if { |resource| should[:ignore].find_index { |ignore| resource.rsapi_current_state[:line].match(ignore) } } if should[:ignore]

    # Remove rules that were (presumably) not put in by puppet
    rules_resources.delete_if { |resource| resource.rsapi_current_state[:name].match(%r{^(\d+)[[:graph:][:space:]]})[1].to_i >= 9000 } if should[:ignore_foreign]

    # We mark all remaining rules for deletion, and then let the catalog override us on rules which should be present
    # We also ensure that the generate rules have the correct protocol to avoid issues with our validation
    rules_resources.each do |resource|
      resource[:ensure] = :absent
      resource[:protocol] = protocol
    end

    rules_resources
  end
end