Class: PuppetX::SIMP::IPTables

Inherits:
Object
  • Object
show all
Defined in:
lib/puppetx/simp/iptables.rb

Instance Method Summary collapse

Constructor Details

#initialize(rules_str) ⇒ IPTables

Returns a new instance of IPTables.



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
# File 'lib/puppetx/simp/iptables.rb', line 5

def initialize(rules_str)
  # The @tables Hash is a way to reference arrays of rules based on the
  # table that contains the rule.

  @tables = {}

  new_rules = []

  current_table = nil
  rules_str.chomp.split("\n").each do |rule|
    # Skip Inline Comments
    next if %r{^\s*(#.*)?$}.match?(rule)

    if %r{^\s*\*}.match?(rule)
      # New tables means new rules
      @tables[current_table][:rules] = sort_rules(new_rules) unless new_rules.empty?

      current_table = add_table(rule)

      new_rules = []

      next
    end

    next unless current_table
    rule = PuppetX::SIMP::IPTables::Rule.new(rule, current_table)

    if rule
      if rule.rule_type == :chain
        @tables[current_table][:chains] << rule
      elsif rule.rule_type == :rule
        new_rules << rule
      end
    end
  end

  # Need to sort the last set of rules in the stack
  @tables[current_table][:rules] = sort_rules(new_rules) unless new_rules.empty?

  prune_chains!
end

Instance Method Details

#==(other) ⇒ Object

A comparator for two rulesets



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/puppetx/simp/iptables.rb', line 48

def ==(other)
  # Fast matching
  unless tables == other.tables
    Puppet.debug('Tables differ between rulesets')
    return false
  end

  unless report == other.report
    Puppet.debug('Reports differ between rulesets')
    return false
  end

  # Comprehensive search
  tables.each do |table|
    unless rules(table) == other.rules(table)
      Puppet.debug("Rules in table '#{table}' differ")
      return false
    end
  end
end

#add_chains(table, new_chains) ⇒ Object

Add chains to the passed table

Parameters:

  • table (String)

    The table to which to add the chains

  • new_chains (Array[<PuppetX::SIMP::IPTables::Rule, String>])

    Chains that should be added to the table



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/puppetx/simp/iptables.rb', line 159

def add_chains(table, new_chains)
  Array(new_chains).each do |chain|
    if chain.is_a?(String)
      chain = if chain[0].chr == ':'
                PuppetX::SIMP::IPTables::Rule.new(chain, table)
              else
                PuppetX::SIMP::IPTables::Rule.new(':' + chain + ' - [0:0]', table)
              end
    end

    raise "chain must be a PuppetX::SIMP::IPTables::Rule or a String not #{chain.class}" unless chain.is_a?(PuppetX::SIMP::IPTables::Rule)
    process_chain(chain)

    unless chains(table).find { |x| x.chain == chain.chain }
      @tables[table][:chains].push(chain)
    end
  end
end

#add_table(table) ⇒ String

Add the passed ‘table’ to the list of tables

Parameters:

  • table (String)

Returns:

  • (String)

    The passed table



133
134
135
136
137
138
# File 'lib/puppetx/simp/iptables.rb', line 133

def add_table(table)
  raise "Table '#{table}' must start with a '*'" unless table =~ %r{^\s*(\*.*)}
  @tables[::Regexp.last_match(1).strip] ||= { chains: [], rules: [] }

  table
end

#append_rules(table, new_rules) ⇒ Object

Append rules to the existing rules while preserving chain order

Will not add a duplicate rule to the chain

Parameters:

  • table (String)

    The table to which to add the chains

  • new_rules (Array[<PuppetX::SIMP::IPTables::Rule, String>])

    Rules that should be appended to the table



251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/puppetx/simp/iptables.rb', line 251

def append_rules(table, new_rules)
  Array(new_rules).each do |rule|
    if rule.is_a?(String)
      rule = PuppetX::SIMP::IPTables::Rule.new(rule)
    end

    raise "rule must be a PuppetX::SIMP::IPTables::Rule not #{rule.class}" unless rule.is_a?(PuppetX::SIMP::IPTables::Rule)
    process_rule(rule)

    if @tables[table][:rules].empty?
      @tables[table][:rules] << rule
    else
      end_index = @tables[table][:rules].rindex { |x| x.chain == rule.chain } || -2

      unless rule == @tables[table][:rules][end_index]
        end_index += 1

        @tables[table][:rules].insert(end_index, rule)
      end
    end
  end
end

#chains(table) ⇒ Array[PuppetX::SIMP::IPTables::Rule]

Return all ‘chains’ for the given table

Parameters:

  • table (String)

    The table from which to return the chains

Returns:



147
148
149
# File 'lib/puppetx/simp/iptables.rb', line 147

def chains(table)
  @tables[table][:chains]
end

#include_rule?(table, rule) ⇒ Boolean

Return whether or not the table contains a matching rule

Parameters:

Returns:

  • (Boolean)

    Boolean



284
285
286
287
288
289
290
# File 'lib/puppetx/simp/iptables.rb', line 284

def include_rule?(table, rule)
  rules(table).each do |current_rule|
    return true if current_rule == rule
  end

  false
end

#merge(iptables_obj) ⇒ PuppetX::SIMP::IPTables

Merge the passed IPTables object with the current object

This involves adding all chains from the passed object and prepending all rules to the existing rules.

Parameters:

Returns:



106
107
108
# File 'lib/puppetx/simp/iptables.rb', line 106

def merge(iptables_obj)
  PuppetX::SIMP::IPTables.new(to_s).merge!(iptables_obj)
end

#merge!(iptables_obj) ⇒ Object

The same behavior as ‘merge` but affecting the current object

Parameters:



115
116
117
118
119
120
121
122
123
124
# File 'lib/puppetx/simp/iptables.rb', line 115

def merge!(iptables_obj)
  iptables_obj.tables.each do |table|
    add_chains(table, iptables_obj.chains(table))
    prepend_rules(table, iptables_obj.rules(table))
  end

  prune_chains!

  self
end

#optimizeObject

Optimize all of the rules and return an optimized object

Optimizations include:

* Elimination of duplicate repeated rules
* Collection of ports into multiport matches in rules where the rest
  of the rule is identical

Returns:

  • PuppetX::SIMP::IPTables



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
# File 'lib/puppetx/simp/iptables.rb', line 422

def optimize
  new_rules = PuppetX::SIMP::IPTables.new('')

  # Hard coded limit in iptables multiport rules.
  max_ports = 15

  tables.each do |table|
    new_rules.add_table(table)
    new_rules.add_chains(table, chains(table))

    existing_rules = rules(table)

    pending_rule = nil

    existing_rules.each_with_index do |rule, index|
      if pending_rule
        prev_ports = []
        new_ports = []
        prev_multiport = false
        port_type = nil

        prev_rule = PuppetX::SIMP::IPTables::Rule.new(
          pending_rule.to_s.split(%r{\s+-}).delete_if { |x|
            retval = false
            if x.empty?
              retval = true
            elsif %r{m(ulti)?port}.match?(x)
              prev_multiport = true
              retval = true
            elsif x =~ %r{(d(estination-)?|s(ource-)?)ports?\s+(.*)}
              port_type = ::Regexp.last_match(1)[0].chr
              prev_ports += ::Regexp.last_match(4).split(',')
              retval = true
            end
            retval
          }.join(' -'), table
        )

        new_rule = PuppetX::SIMP::IPTables::Rule.new(
          rule.to_s.split(%r{\s+-}).delete_if { |x|
            retval = false
            if x.empty? || x =~ %r{m(ulti)?port}
              retval = true
            elsif x =~ %r{(d(estination-)?|s(ource-)?)ports?\s+(.*)}
              # Add ranges as sub-arrays.
              new_ports += ::Regexp.last_match(4).split(',')
              retval = true
            end
            retval
          }.join(' -'), table
        )

        if new_rule == prev_rule
          Puppet.debug("Rule:\n  #{new_rule} matches\n  #{prev_rule}")
          # Flatten when comparing sizes to account for ranges.
          new_ports = (prev_ports + new_ports).uniq

          slice_array(new_ports, max_ports).each do |sub_ports|
            pending_rule = PuppetX::SIMP::IPTables::Rule.new(prev_rule.dup.insert(-2, "m multiport --#{port_type}ports #{sub_ports.sort.uniq.join(',')}").join(' -'))

            # If on the last rule, just write it out
            if index == (existing_rules.count - 1)
              Puppet.debug("Adding: rule '#{pending_rule}' to table '#{table}'")
              new_rules.append_rules(table, pending_rule)
            end
          end
        else
          Puppet.debug("No match for: #{rule}")

          Puppet.debug("Adding: pending rule '#{pending_rule}' to table '#{table}'")
          new_rules.append_rules(table, pending_rule)

          Puppet.debug("Adding: rule '#{rule}' to table '#{table}'")
          new_rules.append_rules(table, rule)

          pending_rule = nil
        end
      elsif (index != (existing_rules.count - 1)) &&
            rule.to_s !~ %r{-p(rotocol)?\s+(ud|tc)p} &&
            rule.to_s !~ %r{--(d(estination-)?|s(ource-)?)ports?\s+}
        # Make sure we have a valid rule for multiport compression.
        #
        # Ignore if we're the last rule since this is only used for
        # pending rule optimization
        pending_rule = rule
      else
        Puppet.debug("Adding: rule '#{rule}' to table '#{table}'")
        new_rules.append_rules(table, rule)

        pending_rule = nil
      end
    end
  end

  new_rules.prune_chains!
end

#prepend_rules(table, new_rules) ⇒ Object

Prepend rules to the existing rules while preserving chain order

Parameters:

  • table (String)

    The table to which to add the chains

  • new_rules (Array[<PuppetX::SIMP::IPTables::Rule, String>])

    Rules that should be prepended to the table



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/puppetx/simp/iptables.rb', line 220

def prepend_rules(table, new_rules)
  Array(new_rules).each do |rule|
    if rule.is_a?(String)
      rule = PuppetX::SIMP::IPTables::Rule.new(rule)
    end

    raise "rule must be a PuppetX::SIMP::IPTables::Rule not #{rule.class}" unless rule.is_a?(PuppetX::SIMP::IPTables::Rule)
    process_rule(rule)

    if @tables[table][:rules].empty?
      @tables[table][:rules] << rule
    else
      start_index = @tables[table][:rules].index { |x| x.chain == rule.chain } || 0

      unless rule == @tables[table][:rules][start_index]
        @tables[table][:rules].insert(start_index, rule)
      end
    end
  end
end

#preserve_match(regex = [], components = ['chain', 'jump', 'input_interface', 'output_interface']) ⇒ Object

Identify rules to be preserved from all tables

Parameters:

  • regex (Array[Regexp]) (defaults to: [])

    A list of regular expressions that should be matched against

  • components (Array[String]) (defaults to: ['chain', 'jump', 'input_interface', 'output_interface'])

    A list of the rule subcomponents that you want to match against all or any of the regular expressions

Returns:

  • PuppetX::SIMP::IPTables



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/puppetx/simp/iptables.rb', line 323

def preserve_match(regex = [], components = ['chain', 'jump', 'input_interface', 'output_interface'])
  result = PuppetX::SIMP::IPTables.new('')

  @tables.each_key do |table|
    # Preserve all existing chains
    result.add_chains(table, chains(table))

    rules(table).each do |rule|
      # Ignore all rules dropped by SIMP
      next if rule.rule_hash['comment'] && rule.rule_hash['comment'][:value].start_with?('SIMP:')

      Array(regex).each do |cmp|
        Array(components).each do |component|
          val = rule.send(component)

          if cmp.match(val)
            result.prepend_rules(table, rule)
          end
        end
      end
    end
  end

  result
end

#prune_chains!Object

Remove any chains from the tables that do not have a matching rule



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/puppetx/simp/iptables.rb', line 179

def prune_chains!
  all_chains = @tables.map { |key, _value| @tables[key][:rules] }
                      .flatten.map { |rule|
    new_rule = [rule.chain]
    new_rule << rule.jump if rule.jump
  }.flatten.uniq

  tables.each do |table|
    chains_to_keep = []

    chains(table).each do |chain|
      if all_chains.include?(chain.chain)
        chains_to_keep << chain
      end
    end

    @tables[table][:chains] = chains_to_keep
  end

  self
end

#report(to_ignore = [], enable_tracking = true) ⇒ Hash

Produces a hash-based report on the number of iptables rules, and type of operation in a given chain.

The report hash is simply a key to count match of each of the different rule types for ease of comparison.

You may optionally pass an array of compiled regular expressions. If this array is present, all items with an interface, chain, or jump matching the regex will be ignored.

Parameters:

  • to_ignore (Array[Regexp]) (defaults to: [])

    Regular expressions that should be ignored

Returns:

  • (Hash)


364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
# File 'lib/puppetx/simp/iptables.rb', line 364

def report(to_ignore = [], enable_tracking = true)
  result = {}

  tables.each do |table|
    result[table] ||= {}

    rules(table).each do |rule|
      ignore_rule = false
      to_ignore.each do |ignore|
        if [rule.chain, rule.jump, rule.input_interface, rule.output_interface].find { |x| ignore.match(x) }
          ignore_rule = true
          break
        end
      end

      next if ignore_rule

      result[table][rule.chain] ||= {}

      # Need a unique key target for precise matches
      tgt_key = [rule.input_interface, rule.output_interface, rule.jump].compact.join('|')

      next if tgt_key.empty?
      result[table][rule.chain][tgt_key] ||= {}
      result[table][rule.chain][tgt_key][:count] ||= 0
      result[table][rule.chain][tgt_key][:count] += 1

      # Better effort matching that just counts which should pick up
      # basic changes but isn't near perfect

      # These get stripped out by the underlying iptables
      listen_all = [ '0.0.0.0/0', '::/0', '::0/0' ]
      rule_parts = rule.to_s.scan(%r{(?:\s(?:tcp|udp|udplite|icmp|esp|ah|sctp|mh|(?:\d\.|\S:).+?/\d+)|-\S+?port.+?\d+)(?:\s|$)}).map(&:strip) - listen_all

      unless rule_parts.empty?
        result[table][rule.chain][:parts] ||= Set.new
        result[table][rule.chain][:parts] = Set.new(rule_parts)
      end

      next unless enable_tracking
      # Best, but fragile, matching
      result[table][rule.chain][:tracking] ||= Set.new
      result[table][rule.chain][:tracking] << rule.rule_hash
    end
  end

  result
end

#rules(table) ⇒ Array[PuppetX::SIMP::IPTables::Rule]

Return all ‘rules’ for the given table

Parameters:

  • table (String)

    The table from which to return the rules

Returns:



208
209
210
# File 'lib/puppetx/simp/iptables.rb', line 208

def rules(table)
  @tables[table][:rules]
end

#sort_rules(to_sort) ⇒ Array[PuppetX::SIMP::IPTables::Rule]

Sort a list of rules into the same order that ‘iptables-save` uses on output

Parameters:

Returns:



76
77
78
79
80
81
82
83
84
85
86
# File 'lib/puppetx/simp/iptables.rb', line 76

def sort_rules(to_sort)
  ordered_chains = to_sort.map { |r| r.chain }.uniq

  ordered_rules = []

  ordered_chains.each do |chain|
    ordered_rules += to_sort.select { |r| r.chain == chain }
  end

  ordered_rules
end

#tablesArray[String]

Return all IPTables ‘tables’

Returns:

  • (Array[String])


92
93
94
# File 'lib/puppetx/simp/iptables.rb', line 92

def tables
  @tables.keys.sort
end

#to_sString

Return a string that is ready for processing by the ‘iptables-restore` command

Returns:

  • (String)


297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/puppetx/simp/iptables.rb', line 297

def to_s
  result = []

  tables.each do |table|
    result << table

    result += chains(table).map(&:rule)
    result += rules(table).map(&:rule)

    result << 'COMMIT'
  end

  result.join("\n")
end