class Tablomat::IPTables

The IPTables interface

Attributes

active[R]
builtin_chains[R]
iptables_bin[RW]
tmp_chain[R]

Public Class Methods

new() click to toggle source
# File lib/tablomat/iptables.rb, line 16
def initialize
  # iptables must be in PATH
  @iptables_bin = 'iptables'
  @iptables_bin = "sudo #{@iptables_bin}" if Etc.getlogin != 'root'

  # get random value for rule creation hack, iptables limits name to 28 characters
  @tmp_chain_prefix = 'tablomat'

  @builtin_chains = { filter: %w[INPUT FORWARD OUTPUT], nat: %w[PREROUTING INPUT OUTPUT POSTROUTING], mangle: %w[PREROUTING INPUT FORWARD OUTPUT POSTROUTING] }
  @tables = {}

  @active = true
  synchronize
end

Public Instance Methods

activate() click to toggle source
# File lib/tablomat/iptables.rb, line 146
def activate
  @active = true
  @tables.each do |_name, table|
    table.activate
  end
end
append(table_name, chain_name, data) click to toggle source
# File lib/tablomat/iptables.rb, line 136
def append(table_name, chain_name, data)
  data = normalize data, table_name
  table(table_name).append(chain_name, data)
end
deactivate() click to toggle source
# File lib/tablomat/iptables.rb, line 153
def deactivate
  @active = false
  tables.each do |_name, table|
    table.deactivate
  end
end
delete(table_name, chain_name, data) click to toggle source
# File lib/tablomat/iptables.rb, line 141
def delete(table_name, chain_name, data)
  data = normalize data, table_name
  table(table_name).delete(chain_name, data)
end
exec(cmd) click to toggle source
# File lib/tablomat/iptables.rb, line 31
def exec(cmd)
  Exec.exec(cmd)
end
exists(table_name, chain_name, data = nil) click to toggle source

rubocop:disable Metrics/AbcSize

# File lib/tablomat/iptables.rb, line 121
def exists(table_name, chain_name, data = nil)
  if data.nil?
    table(table_name).chain_exists(chain_name) && table(table_name).chain(chain_name).active
  else
    data = normalize data, table_name
    table(table_name).chain(chain_name).rules.count { |_k, v| normalize(v.description, table_name) == data && v.active } >= 1
  end
end
get_active_rules(table = 'nat', chain = 'PREROUTING') click to toggle source

rubocop:disable Metrics/AbcSize

# File lib/tablomat/iptables.rb, line 161
def get_active_rules(table = 'nat', chain = 'PREROUTING')
  rrgx = /(?<key>--?[[:alpha:]-]+) (?<value>[[:alnum:]:\.]+)/.freeze
  switch_map = { '-s' => :source, '--source' => :source,
                 '-d' => :destination, '--destination' => :destination,
                 '--dport' => :dport, '--to-destination' => :to_dest,
                 '-p' => :protocol, '--protocol' => :protocol,
                 '-j' => :jump, '--jump' => :jump,
                 '--match-set' => :match }

  table(table).chain(chain).rules.filter { |_, r| r.active }.map do |_, rule|
    rule.description.to_enum(:scan, rrgx)
        .map { Regexp.last_match }
        .map { |m| [switch_map[m[:key]], m[:value]] }
        .filter { |m| m[0] }.to_h
  end
end
insert(table_name, chain_name, data, pos) click to toggle source

rubocop:enable Metrics/AbcSize

# File lib/tablomat/iptables.rb, line 131
def insert(table_name, chain_name, data, pos)
  data = normalize data, table_name
  table(table_name).insert(chain_name, data, pos)
end
normalize(data, table = 'filter') click to toggle source

converts any given rule to the iptables internal description format by using a temporary table rubocop:disable Metrics/AbcSize

# File lib/tablomat/iptables.rb, line 103
def normalize(data, table = 'filter')
  synchronize
  tmp_chain = "#{@tmp_chain_prefix}#{Digest::SHA256.hexdigest Random.rand(1024).to_s}"[0, 28]
  self.table(table).chain(tmp_chain).activate
  self.table(table).append(tmp_chain, data)
  synchronize
  normalized = data
  self.table(table).chain(tmp_chain).rules.select { |_k, r| r.owned == false }.each do |_k, r|
    normalized = r.description
  end
  self.table(table).delete(tmp_chain, data)
  self.table(table).chain(tmp_chain).deactivate
  self.table(table).chain(tmp_chain).apply_delete
  normalized
end
parse_chain(chain, rules) click to toggle source
# File lib/tablomat/iptables.rb, line 86
def parse_chain(chain, rules)
  rules.each do |rule|
    r = chain.rule(rule, false)
    r.activate true
  end
end
parse_data(data) click to toggle source

rubocop:enable Metrics/AbcSize

# File lib/tablomat/iptables.rb, line 70
def parse_data(data)
  data.each do |table, chains|
    t = self.table(table, false)
    t.activate true
    parse_table(t, chains)
  end
end
parse_output(stdout) click to toggle source

rubocop:disable Metrics/AbcSize

# File lib/tablomat/iptables.rb, line 49
def parse_output(stdout)
  stdout = stdout.split("\n").reject { |s| s[0] == '#' }.join("\n")
  data = {}
  stdout.split(/^\*/).reject { |s| s == '' }.each do |ruleset|
    table, rule = ruleset.match(/(.+?)\n(.*)/m).to_a[1..-1]
    data[table] = {}

    raise "Empty ruleset in table #{table}" if table.nil?

    rule.scan(/^:(.+?)\s+/).each do |match|
      data[table][match[0]] = []
    end

    rule.scan(/^-A (.+?) (.+?)\n/).each do |match|
      data[table][match[0]] << match[1]
    end
  end
  parse_data(data)
end
parse_table(table, chains) click to toggle source
# File lib/tablomat/iptables.rb, line 78
def parse_table(table, chains)
  chains.each do |chain, rules|
    c = table.chain(chain, false)
    c.activate true
    parse_chain(c, rules)
  end
end
print() click to toggle source

rubocop:enable Metrics/AbcSize

switch_sources(old_src, new_src) click to toggle source

used to easily switch rules from one src ip to another

# File lib/tablomat/iptables.rb, line 179
def switch_sources(old_src, new_src)
  @tables.to_a.each do |_kt, tbl|
    tbl.chains.to_a.each do |_kc, chn|
      next if chn.name.include? 'tablomat'

      chn.rules.to_a.each do |_key, rule|
        next unless rule.description.include? "-s #{old_src}"

        new_data = normalize(rule.description, tbl.name).sub "-s #{old_src}", "-s #{new_src}"
        pos = rule.position + 1
        delete(tbl.name, chn.name, rule.description)
        insert(tbl.name, chn.name, new_data, pos)
      end
    end
  end
end
synchronize() click to toggle source
# File lib/tablomat/iptables.rb, line 35
def synchronize
  # called regularly by normalize
  command = "#{@iptables_bin}-save"
  stdout = `#{command} 2>&1`.strip << "\n"
  if $CHILD_STATUS != 0
    # throw error
    puts "Invalid return value when calling #{command}"
  end
  parse_output stdout
rescue StandardError => e
  puts "[error] #{e}"
end
table(name, owned = true, &block) click to toggle source
# File lib/tablomat/iptables.rb, line 93
def table(name, owned = true, &block)
  name = name.to_s.downcase
  (@tables[name] || Table.new(self, name, owned)).tap do |table|
    @tables[name] = table
    block&.call(table)
  end
end