# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
#
# Copyright (c) 2016-present, Facebook, Inc.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

module FB
  module Iptables
    # rubocop:disable Style/MutableConstant
    # We let this particular constant remain mutable in case we need to shove in
    # a nat table or something.
    TABLES_AND_CHAINS = {
      'mangle' => %w{PREROUTING INPUT OUTPUT FORWARD POSTROUTING},
      'filter' => %w{INPUT OUTPUT FORWARD},
      'raw' => %w{PREROUTING OUTPUT},
    }
    # rubocop:enable Style/MutableConstant

    # Facebook, like others does not have NAT in their kernels. However many
    # people do, so provide an easy way to initialize the various structures
    # with NAT entries.
    #
    # Note this is idempotent - you can call it many times, it will not
    # overwrite user rules
    def self.enable_nat(node)
      TABLES_AND_CHAINS['nat'] ||= %w{PREROUTING INPUT OUTPUT POSTROUTING}

      # normally this would be auto-vivified, but since we want to check
      # fb_iptables.nat.$chain below, this has to exist
      unless node['fb_iptables']['nat']
        node.default['fb_iptables']['nat'] = {}
      end

      TABLES_AND_CHAINS['nat'].each do |chain|
        next if node['fb_iptables']['nat'][chain]

        node.default['fb_iptables']['nat'][chain] = {
          'policy' => 'ACCEPT',
          'rules' => {},
        }
      end

      unless node['fb_iptables']['dynamic_chains']['nat']
        node.default['fb_iptables']['dynamic_chains']['nat'] = {}
      end
    end

    # Is the given rule valid for the give ip version
    def self.rule_supports_ip_version?(rule, version)
      return true unless rule['ip']
      return true if rule['ip'] == version

      rule['ip'].is_a?(Array) && rule['ip'].include?(version)
    end

    def self.each_table(for_ip, node)
      FB::Iptables::TABLES_AND_CHAINS.each_key do |table|
        chains = node['fb_iptables'][table].to_hash
        # 'only' is a purposefully not publicly documented attribute
        # thou can to a table, to have this table only used for a
        # specific ip version
        # It allows you for example to remove any mention of the
        # *nat table in ip6tables if the box only have the ipv4 nat
        # module installed.
        #
        # node.default['fb_iptables']['nat']['only'] = 4
        # The nat table chains/policies won't be written in
        # /etc/sysconfig/ip6tables

        # We delete it not to have a 'only' table
        only = chains.delete('only')
        if only.nil? || only == for_ip
          yield(table, chains)
        end
      end
    end

    # walk dynamic chains and return all dynamic chains enabled for this
    # table/chain combination.
    #
    # For example, for filter/INPUT, we include any chains under 'filter' with
    # 'INPUT' in it's enabled list.
    def self.get_dynamic_chains(table, chain, node)
      dynamic = node['fb_iptables']['dynamic_chains'][table].to_hash
      dynamic.map do |dynamic_chain, enabled_for|
        dynamic_chain if enabled_for.include?(chain)
      end.compact
    end

    # Return true if the iptables modules are loaded for the specified
    # IP version. This check is based on examining
    # /proc/net/ip#_tables_names; if the file has contents then the
    # iptables modules are loaded.
    def self.iptables_active?(ip_version)
      if ip_version == 4
        procfile = '/proc/net/ip_tables_names'
      else
        procfile = '/proc/net/ip6_tables_names'
      end
      File.exist?(procfile) && !File.read(procfile).empty?
    end
  end
end
