232 lines
7.6 KiB
Ruby
232 lines
7.6 KiB
Ruby
#
|
|
# Cookbook Name:: firewall
|
|
# Provider:: rule_iptables
|
|
#
|
|
# Copyright 2012, computerlyrik
|
|
#
|
|
# 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.
|
|
#
|
|
require 'poise'
|
|
|
|
class Chef
|
|
class Provider::FirewallRuleIptables < Provider
|
|
include Poise
|
|
include Chef::Mixin::ShellOut
|
|
include FirewallCookbook::Helpers
|
|
|
|
def action_allow
|
|
apply_rule(:allow)
|
|
end
|
|
|
|
def action_deny
|
|
apply_rule(:deny)
|
|
end
|
|
|
|
def action_reject
|
|
apply_rule(:reject)
|
|
end
|
|
|
|
def action_redirect
|
|
apply_rule(:redirect)
|
|
end
|
|
|
|
def action_masquerade
|
|
apply_rule(:masquerade)
|
|
end
|
|
|
|
def action_log
|
|
apply_rule(:log)
|
|
end
|
|
|
|
def action_remove
|
|
# TODO: specify which target to delete
|
|
# for now this will remove raw + all targeted lines
|
|
remove_rule(:allow)
|
|
remove_rule(:deny)
|
|
remove_rule(:reject)
|
|
remove_rule(:redirect)
|
|
remove_rule(:masquerade)
|
|
end
|
|
|
|
private
|
|
|
|
CHAIN = { :in => 'INPUT', :out => 'OUTPUT', :pre => 'PREROUTING', :post => 'POSTROUTING' } unless defined? CHAIN # , nil => "FORWARD"}
|
|
TARGET = { :allow => 'ACCEPT', :reject => 'REJECT', :deny => 'DROP', :masquerade => 'MASQUERADE', :redirect => 'REDIRECT', :log => 'LOG --log-prefix "iptables: " --log-level 7' } unless defined? TARGET
|
|
|
|
def apply_rule(type = nil)
|
|
firewall_commands = determine_iptables_commands
|
|
firewall_commands.each do |firewall_command|
|
|
ipv6 = (firewall_command == 'ip6tables') ? true : false
|
|
if new_resource.position
|
|
firewall_command << ' -I '
|
|
else
|
|
firewall_command << ' -A '
|
|
end
|
|
|
|
# TODO: implement logging for :connections :packets
|
|
firewall_rule = build_firewall_rule(type)
|
|
|
|
Chef::Log.debug("#{new_resource}: #{firewall_rule}")
|
|
if rule_exists?(firewall_rule, ipv6)
|
|
Chef::Log.info("#{new_resource} #{type} rule exists... won't apply")
|
|
else
|
|
cmdstr = firewall_command + firewall_rule
|
|
converge_by("firewall_rule[#{new_resource.name}] #{firewall_rule}") do
|
|
notifying_block do
|
|
shell_out!(cmdstr) # shell_out! is already logged
|
|
new_resource.updated_by_last_action(true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def remove_rule(type = nil)
|
|
firewall_commands = determine_iptables_commands
|
|
firewall_commands.each do |firewall_command|
|
|
ipv6 = (firewall_command == 'ip6tables') ? true : false
|
|
|
|
# TODO: implement logging for :connections :packets
|
|
firewall_rule = build_firewall_rule(type)
|
|
|
|
Chef::Log.debug("#{new_resource}: #{firewall_rule}")
|
|
if rule_exists?(firewall_rule, ipv6)
|
|
cmdstr = firewall_command + firewall_rule
|
|
converge_by("firewall_rule[#{new_resource.name}] #{firewall_rule}") do
|
|
notifying_block do
|
|
shell_out!(cmdstr) # shell_out! is already logged
|
|
new_resource.updated_by_last_action(true)
|
|
end
|
|
end
|
|
else
|
|
Chef::Log.info("#{new_resource} #{type} rule does not exist... won't remove")
|
|
end
|
|
end
|
|
end
|
|
|
|
def ipv4_rule?
|
|
if (new_resource.source && IPAddr.new(new_resource.source).ipv4?) ||
|
|
(new_resource.destination && IPAddr.new(new_resource.destination).ipv4?)
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def ipv6_rule?
|
|
if (new_resource.source && IPAddr.new(new_resource.source).ipv6?) ||
|
|
(new_resource.destination && IPAddr.new(new_resource.destination).ipv6?)
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
def determine_iptables_commands
|
|
if ipv4_rule?
|
|
commands = ['iptables']
|
|
elsif ipv6_rule?
|
|
commands = ['ip6tables']
|
|
else # no source or destination address, add rules for both ipv4 and ipv6
|
|
commands = %w(iptables ip6tables)
|
|
end
|
|
commands
|
|
end
|
|
|
|
def build_firewall_rule(type = nil)
|
|
if new_resource.raw
|
|
firewall_rule = new_resource.raw.strip
|
|
else
|
|
firewall_rule = ''
|
|
if new_resource.direction
|
|
firewall_rule << "#{CHAIN[new_resource.direction.to_sym]} "
|
|
else
|
|
firewall_rule << 'FORWARD '
|
|
end
|
|
firewall_rule << "#{new_resource.position} " if new_resource.position
|
|
|
|
if [:pre, :post].include?(new_resource.direction)
|
|
firewall_rule << '-t nat '
|
|
end
|
|
|
|
# Iptables order of prameters is important here see example output below:
|
|
# -A INPUT -s 1.2.3.4/32 -d 5.6.7.8/32 -i lo -p tcp -m tcp -m state --state NEW -m comment --comment "hello" -j DROP
|
|
firewall_rule << "-s #{ip_with_mask(new_resource.source)} " if new_resource.source && new_resource.source != '0.0.0.0/0'
|
|
firewall_rule << "-d #{new_resource.destination} " if new_resource.destination
|
|
|
|
firewall_rule << "-i #{new_resource.interface} " if new_resource.interface
|
|
firewall_rule << "-o #{new_resource.dest_interface} " if new_resource.dest_interface
|
|
|
|
firewall_rule << "-p #{new_resource.protocol} " if new_resource.protocol
|
|
firewall_rule << '-m tcp ' if new_resource.protocol.to_sym == :tcp
|
|
|
|
# using multiport here allows us to simplify our greps and rule building
|
|
firewall_rule << "-m multiport --sports #{port_to_s(new_resource.source_port)} " if new_resource.source_port
|
|
firewall_rule << "-m multiport --dports #{port_to_s(dport_calc)} " if dport_calc
|
|
|
|
firewall_rule << "-m state --state #{new_resource.stateful.is_a?(Array) ? new_resource.stateful.join(',').upcase : new_resource.stateful.upcase} " if new_resource.stateful
|
|
firewall_rule << "-m comment --comment \"#{new_resource.description}\" "
|
|
firewall_rule << "-j #{TARGET[type]} "
|
|
firewall_rule << "--to-ports #{new_resource.redirect_port} " if type == 'redirect'
|
|
firewall_rule.strip!
|
|
end
|
|
firewall_rule
|
|
end
|
|
|
|
def rule_exists?(rule, ipv6 = false)
|
|
fail 'no rule supplied' unless rule
|
|
if new_resource.position
|
|
detect_rule = rule.gsub(/#{CHAIN[new_resource.direction]}\s(\d+)/, '\1' + " -A #{CHAIN[new_resource.direction]}")
|
|
else
|
|
detect_rule = rule
|
|
end
|
|
|
|
# match quotes generously
|
|
detect_rule = detect_rule.gsub(/'/, "'*")
|
|
detect_rule = detect_rule.gsub(/"/, '"*')
|
|
|
|
line_number = 0
|
|
match = shell_out!(ipv6 ? 'ip6tables-save' : 'iptables-save').stdout.lines.find do |line|
|
|
next if line !~ /#{CHAIN[new_resource.direction]}/
|
|
next if line[0] != '-'
|
|
line_number += 1
|
|
line = "#{line_number} #{line}" if new_resource.position
|
|
# Chef::Log.debug("matching: [#{detect_rule}] to [#{line.chomp.rstrip}]")
|
|
line =~ /#{detect_rule}/
|
|
end
|
|
|
|
match
|
|
rescue Mixlib::ShellOut::ShellCommandFailed
|
|
Chef::Log.debug("#{new_resource} check fails with: " + match.inspect)
|
|
Chef::Log.debug("#{new_resource} assuming #{rule} rule does not exist")
|
|
false
|
|
end
|
|
|
|
def dport_calc
|
|
new_resource.dest_port || new_resource.port
|
|
end
|
|
|
|
def ip_with_mask(ip)
|
|
if ip.include?('/')
|
|
ip
|
|
elsif ipv4_rule?
|
|
"#{ip}/32"
|
|
elsif ipv6_rule?
|
|
"#{ip}/128"
|
|
else
|
|
ip
|
|
end
|
|
end
|
|
end
|
|
end
|