1
0
mirror of https://github.com/OpenNebula/one.git synced 2025-03-21 14:50:08 +03:00

Feature #3175: Improve SG driver

- Refactor and apply new ACCEPT => REJECT strategy
- Adds icmp_type support
- Do not apply duplicate rules
- Specific ipset per protocol
- Improve rule types readibility in the driver
- Adds support for mac and ip spoofing filtering
- Reorder rules
This commit is contained in:
Jaime Melis 2014-11-25 10:48:26 +01:00
parent b34dbfd7fc
commit 60634c34f3
2 changed files with 368 additions and 195 deletions

View File

@ -389,36 +389,19 @@ var create_security_group_wizard_html =
</label>\
<select class="security_group_rule_icmp_type">\
<option value="" selected="selected">'+tr("All")+'</option>\
<option value = "0">'+"0: Echo Reply"+'</option>\
<option value = "0">'+"0/0: Echo Reply (pong)"+'</option>\
<option value = "3">'+"3: Destination Unreachable"+'</option>\
<option value = "4">'+"4: Source Quench"+'</option>\
<option value = "4">'+"4/0: Source Quench"+'</option>\
<option value = "5">'+"5: Redirect"+'</option>\
<option value = "6">'+"6: Alternate Host Address"+'</option>\
<option value = "8">'+"8: Echo"+'</option>\
<option value = "9">'+"9: Router Advertisement"+'</option>\
<option value = "10">'+"10: Router Solicitation"+'</option>\
<option value = "8">'+"8/0: Echo Request (ping)"+'</option>\
<option value = "9">'+"9/0: Router Advertisement"+'</option>\
<option value = "10">'+"10/0: Router Solicitation"+'</option>\
<option value = "11">'+"11: Time Exceeded"+'</option>\
<option value = "12">'+"12: Parameter Problem"+'</option>\
<option value = "13">'+"13: Timestamp"+'</option>\
<option value = "14">'+"14: Timestamp Reply"+'</option>\
<option value = "15">'+"15: Information Request"+'</option>\
<option value = "16">'+"16: Information Reply"+'</option>\
<option value = "17">'+"17: Address Mask Request"+'</option>\
<option value = "18">'+"18: Address Mask Reply"+'</option>\
<option value = "30">'+"30: Traceroute"+'</option>\
<option value = "31">'+"31: Datagram Conversion Error"+'</option>\
<option value = "32">'+"32: Mobile Host Redirect"+'</option>\
<option value = "33">'+"33: IPv6 Where-Are-You"+'</option>\
<option value = "34">'+"34: IPv6 I-Am-Here"+'</option>\
<option value = "35">'+"35: Mobile Registration Request"+'</option>\
<option value = "36">'+"36: Mobile Registration Reply"+'</option>\
<option value = "37">'+"37: Domain Name Request"+'</option>\
<option value = "38">'+"38: Domain Name Reply"+'</option>\
<option value = "39">'+"39: SKIP"+'</option>\
<option value = "40">'+"40: Photuris"+'</option>\
<option value = "41">'+"41: ICMP messages utilized by experimental mobility protocols such as Seamoby"+'</option>\
<option value = "253">'+"253: RFC3692-style Experiment 1"+'</option>\
<option value = "254">'+"254: RFC3692-style Experiment 2"+'</option>\
<option value = "13">'+"13/0: Timestamp Request"+'</option>\
<option value = "14">'+"14/0: Timestamp Reply"+'</option>\
<option value = "17">'+"17/0: Address Mask Request"+'</option>\
<option value = "18">'+"18/0: Address Mask Reply"+'</option>\
</select>\
</div>\
</div>\
@ -918,7 +901,7 @@ function updateSecurityGroupInfo(request,security_group){
}
function insert_sg_rules_table(sg){
var html =
var html =
'<table class="policies_table dataTable">\
<thead>\
<tr>\

View File

@ -14,6 +14,10 @@
# limitations under the License. #
#--------------------------------------------------------------------------- #
# TODO: remove
require 'rubygems'
require 'pp'
################################################################################
# IP and NETMASK Library
################################################################################
@ -201,11 +205,14 @@ end
# SecurityGroups and Rules
################################################################################
# TODO: remove
require 'colorator'
class CommandsError < StandardError; end
class Commands
def initialize
@commands = []
clear!
end
def add(cmd)
@ -228,18 +235,33 @@ class Commands
out = ""
@commands.each{|c|
out << `#{c}`
# TODO: remove
puts "=> #{c}".green
c_out = `#{c}`
puts c_out if !c_out.empty?
out << c_out
if !$?.success?
@commands = []
clear!
raise CommandsError.new(c), "Command Error: #{c}"
end
}
@commands = []
clear!
out
end
def uniq!
@commands.uniq!
end
def clear!
@commands = []
end
def to_a
@commands
end
@ -248,6 +270,25 @@ end
class RuleError < StandardError; end
class Rule
TYPES = {
# PROTOCOL, RULE_TYPE, NET, RANGE, ICMP_TYPE
[ 1, 1, 0, 0, 0 ] => :protocol,
[ 1, 1, 0, 1, 0 ] => :portrange,
[ 1, 1, 0, 0, 1 ] => :icmp_type,
[ 1, 1, 1, 0, 0 ] => :net,
[ 1, 1, 1, 1, 0 ] => :net_portrange,
[ 1, 1, 1, 0, 1 ] => :net_icmp_type
}
ICMP_TYPES = %w{3 5 11 12 0 4 8 9 10 13 14 17 18}
ICMP_TYPES_EXPANDED = {
3 => [0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15],
5 => [0, 1, 2, 3],
11 => [0, 1],
12 => [0, 1]
}
def initialize(rule)
@rule = rule
@commands = Commands.new
@ -273,7 +314,7 @@ class Rule
end
def range
@rule[:range] || nil
@rule[:range]
end
def net
@ -283,13 +324,31 @@ class Rule
r.get_nets.collect{|n| n.to_s}
end
def icmp_type
@rule[:icmp_type]
end
def icmp_type_expand
if (codes = ICMP_TYPES_EXPANDED[icmp_type.to_i])
codes.collect{|e| "#{icmp_type}/#{e}"}
else
["#{icmp_type}/0"]
end
end
# Helper
def valid?
valid = true
error_message = []
if !protocol || ![:tcp, :udp, :icmp, :esp].include?(protocol)
if type.nil?
error_message << "Invalid combination of rule attributes: "
error_message << type(true).to_s
valid = false
end
if !protocol || ![:all, :tcp, :udp, :icmp, :esp].include?(protocol)
error_message << "Invalid protocol: #{protocol}"
valid = false
end
@ -304,8 +363,17 @@ class Rule
valid = false
end
if range && protocol == :esp
error_message << "IPSEC does not support port ranges"
if icmp_type && !ICMP_TYPES.include?(icmp_type)
error_message << "ICMP Type '#{icmp_type}' not supported. Valid list is '#{ICMP_TYPES.join(',')}'"
end
if icmp_type && !(protocol == :icmp)
error_message << "Protocol '#{protocol}' does not support ICMP TYPES"
valid = false
end
if range && ![:tcp, :udp].include?(protocol)
error_message << "Protocol '#{protocol}' does not support port ranges"
valid = false
end
@ -321,6 +389,50 @@ class Rule
@rule[:ip].match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) && \
@rule[:size].match(/^\d+$/)
end
# Returns the rule type. Rules currently support these (final and relevant)
# attributes.
#
# PROTOCOL (mandatory)
# - Specifies the protocol of the rule
# - values: ['ALL', 'TCP', 'UDP', 'ICMP', 'IPSEC']
#
# RULE_TYPE (mandatory)
# - Specifies the direction of application of the rule
# - values: ['INBOUND', 'OUTBOUND']
#
# RANGE (optional)
# - only works for protocols ['TCP', 'UDP']
# - uses the iptables multiports syntax
#
# ICMP_TYPE (optional)
# - Only works for protocol 'ICMP'
# - Is either in the form of '<TYPE>' or '<TYPE>/<CODE>', where both
# '<TYPE>' and '<CODE>' are integers. This class has a helper method
# tgat expands '<TYPE>' into all the '<TYPE>/<CODE>' subtypes.
#
# IP and SIZE (optional but must be specified together)
# - Can be applied to any protocol
# - IP is the first valid IP and SIZE is the number of consecutive IPs
#
# Depending on the combination of these attributes we can obtaine 4 rule
# rule types (some with subtypes):
#
# ['PROTOCOL', 'RULE_TYPE'] => Type 1: 'protocol'
# ['PROTOCOL', 'RULE_TYPE', 'RANGE'] => Type 2A: 'portrange'
# ['PROTOCOL', 'RULE_TYPE', 'ICMP_TYPE'] => Type 2B: 'icmp_type'
# ['PROTOCOL', 'RULE_TYPE', 'IP', 'SIZE'] => Type 3: 'net'
# ['PROTOCOL', 'RULE_TYPE', 'IP', 'SIZE', 'RANGE'] => Type 4A: 'net_portrange'
# ['PROTOCOL', 'RULE_TYPE', 'IP', 'SIZE', 'ICMP_TYPE'] => Type 4B: 'net_icmp_type'
#
# @return [Symbol] The rule type
def type(only_key = false)
key = [protocol, rule_type, net, range, icmp_type].collect do |e|
!!e ? 1 : 0
end
only_key ? key : TYPES[key]
end
end
class SecurityGroup
@ -343,93 +455,67 @@ end
################################################################################
class SecurityGroupIPTables < SecurityGroup
# RULE_CLASS = IPTablesRule
GLOBAL_CHAIN = "opennebula"
def initialize(vm, nic, sg_id, rules)
super
@commands = Commands.new
chain = "one-sg-#{@sg_id}"
@vars = self.class.vars(@vm, @nic, @sg_id)
vm_id = @vm['ID']
nic_id = @nic[:nic_id]
@chain_in = "one-#{vm_id}-#{nic_id}-i"
@chain_out = "one-#{vm_id}-#{nic_id}-o"
@chain_sg_in = "one-#{vm_id}-#{nic_id}-#{@sg_id}-i"
@chain_sg_out = "one-#{vm_id}-#{nic_id}-#{@sg_id}-o"
end
def bootstrap
# SG chains
@commands.iptables("-N #{@chain_sg_in}")
@commands.iptables("-N #{@chain_sg_out}")
# Redirect Traffic
@commands.iptables("-A #{@chain_in} -j #{@chain_sg_in}")
@commands.iptables("-A #{@chain_out} -j #{@chain_sg_out}")
# IPsets
@commands.ipset("create #{@chain_sg_in}-n hash:net")
@commands.ipset("create #{@chain_sg_out}-n hash:net")
@commands.ipset("create #{@chain_sg_in}-nr hash:net,port")
@commands.ipset("create #{@chain_sg_out}-nr hash:net,port")
@chain_in = @vars[:chain_in]
@chain_out = @vars[:chain_out]
@set_sg_in = @vars[:set_sg_in]
@set_sg_out = @vars[:set_sg_out]
end
def process_rules
@rules.each do |rule|
if !rule.range && !rule.net
# T1 - protocol
case rule.type
when :protocol
chain = rule.rule_type == :inbound ? @chain_in : @chain_out
@commands.iptables("-A #{chain} -p #{rule.protocol} -j RETURN")
when :portrange
chain = rule.rule_type == :inbound ? @chain_in : @chain_out
@commands.iptables("-A #{chain} -p #{rule.protocol} -m multiport --dports #{rule.range} -j RETURN")
when :icmp_type
chain = rule.rule_type == :inbound ? @chain_in : @chain_out
@commands.iptables("-A #{chain} -p icmp --icmp-type #{rule.icmp_type} -j RETURN")
when :net
if rule.rule_type == :inbound
chain = @chain_sg_in
chain = @chain_in
set = "#{@set_sg_in}-#{rule.protocol}-n"
dir = "src"
else
chain = @chain_sg_out
chain = @chain_out
set = "#{@set_sg_out}-#{rule.protocol}-n"
dir = "dst"
end
@commands.iptables("-A #{chain} -p #{rule.protocol} -j ACCEPT")
elsif rule.range && !rule.net
# T2 - port range
if rule.rule_type == :inbound
chain = @chain_sg_in
else
chain = @chain_sg_out
end
@commands.iptables("-A #{chain} -p #{rule.protocol} -m multiport --dports #{rule.range} -j ACCEPT")
elsif !rule.range && rule.net
# T3 - net
if rule.rule_type == :inbound
chain = @chain_sg_in
dir = "src"
else
chain = @chain_sg_out
dir = "dst"
end
set = "#{chain}-n"
@commands.iptables("-A #{chain} -p #{rule.protocol} -m set --match-set #{set} #{dir} -j ACCEPT")
@commands.ipset("create #{set} hash:net")
@commands.iptables("-A #{chain} -p #{rule.protocol} -m set --match-set #{set} #{dir} -j RETURN")
rule.net.each do |n|
@commands.ipset("add -exist #{set} #{n}")
end
elsif rule.range && rule.net
# T4 - net && port range
when :net_portrange
if rule.rule_type == :inbound
chain = @chain_sg_in
chain = @chain_in
set = @set_sg_in + "-nr"
dir = "src,dst"
else
chain = @chain_sg_out
chain = @chain_in
set = @set_sg_in + "-n"
dir = "dst,dst"
end
set = "#{chain}-nr"
@commands.iptables("-A #{chain} -m set --match-set #{set} #{dir} -j ACCEPT")
@commands.ipset("create #{set} hash:net,port")
@commands.iptables("-A #{chain} -m set --match-set #{set} #{dir} -j RETURN")
rule.net.each do |n|
rule.range.split(",").each do |r|
@ -438,13 +524,184 @@ class SecurityGroupIPTables < SecurityGroup
@commands.ipset("add -exist #{set} #{net_range}")
end
end
when :net_icmp_type
if rule.rule_type == :inbound
chain = @chain_in
set = @set_sg_in + "-nr"
dir = "src,dst"
else
chain = @chain_in
set = @set_sg_in + "-n"
dir = "dst,dst"
end
@commands.ipset("create #{set} hash:net,port")
@commands.iptables("-A #{chain} -m set --match-set #{set} #{dir} -j RETURN")
rule.net.each do |n|
rule.icmp_type_expand.each do |type_code|
net_range = "#{n},icmp:#{type_code}"
@commands.ipset("add -exist #{set} #{net_range}")
end if rule.icmp_type_expand
end
end
end
@commands.uniq!
end
def run!
@commands.run!
end
############################################################################
# Static methods
############################################################################
def self.global_bootstrap
info = self.info
if !info[:iptables_s].split("\n").include? "-N #{GLOBAL_CHAIN}"
commands = Commands.new
commands.iptables "-N #{GLOBAL_CHAIN}"
commands.iptables "-A FORWARD -m physdev --physdev-is-bridged -j #{GLOBAL_CHAIN}"
commands.iptables "-A #{GLOBAL_CHAIN} -j DROP"
commands.run!
end
end
def self.nic_pre(vm, nic)
commands = Commands.new
vars = self.vars(vm, nic)
chain = vars[:chain]
chain_in = vars[:chain_in]
chain_out = vars[:chain_out]
# create chains
commands.iptables "-N #{chain_in}" # inbound
commands.iptables "-N #{chain_out}" # outbound
# Send traffic to the NIC chains
commands.iptables"-I #{GLOBAL_CHAIN} -m physdev --physdev-out #{nic[:tap]} --physdev-is-bridged -j #{chain_in}"
commands.iptables"-I #{GLOBAL_CHAIN} -m physdev --physdev-in #{nic[:tap]} --physdev-is-bridged -j #{chain_out}"
# Mac-spofing
if nic[:filter_mac_spoofing]
commands.iptables"-A #{chain_out} -m mac ! --mac-source #{nic[:mac]} -j DROP"
end
# IP-spofing
if nic[:filter_ip_spoofing]
commands.iptables"-A #{chain_out} ! --source #{nic[:ip]} -j DROP"
end
# Related, Established
commands.iptables"-A #{chain_in} -m state --state ESTABLISHED,RELATED -j ACCEPT"
commands.iptables"-A #{chain_out} -m state --state ESTABLISHED,RELATED -j ACCEPT"
commands.run!
end
def self.nic_post(vm, nic)
vars = self.vars(vm, nic)
chain_in = vars[:chain_in]
chain_out = vars[:chain_out]
commands = Commands.new
commands.iptables("-A #{chain_in} -j DROP")
commands.iptables("-A #{chain_out} -j DROP")
commands.run!
end
def self.nic_deactivate(vm, nic)
vars = self.vars(vm, nic)
chain = vars[:chain]
chain_in = vars[:chain_in]
chain_out = vars[:chain_out]
info = self.info
iptables_forwards = info[:iptables_forwards]
iptables_s = info[:iptables_s]
ipset_list = info[:ipset_list]
commands = Commands.new
iptables_forwards.lines.reverse_each do |line|
fields = line.split
if [chain_in, chain_out].include?(fields[1])
n = fields[0]
commands.iptables("-D #{GLOBAL_CHAIN} #{n}")
end
end
remove_chains = []
iptables_s.lines.each do |line|
if line.match(/^-N #{chain}/)
remove_chains << line.split[1]
end
end
remove_chains.each {|c| commands.iptables("-F #{c}") }
remove_chains.each {|c| commands.iptables("-X #{c}") }
ipset_list.lines.each do |line|
if line.match(/^#{chain}/)
set = line.strip
commands.ipset("destroy #{set}")
end
end
commands.run!
end
def self.info
commands = Commands.new
commands.iptables("-S")
iptables_s = commands.run!
if iptables_s.match(/^-N #{GLOBAL_CHAIN}$/)
commands.iptables("-L #{GLOBAL_CHAIN} --line-numbers")
iptables_forwards = commands.run!
else
iptables_forwards = ""
end
commands.ipset("list -name")
ipset_list = commands.run!
{
:iptables_forwards => iptables_forwards,
:iptables_s => iptables_s,
:ipset_list => ipset_list
}
end
def self.vars(vm, nic, sg_id = nil)
vm_id = vm['ID']
nic_id = nic[:nic_id]
vars = {}
vars[:vm_id] = vm_id,
vars[:nic_id] = nic_id,
vars[:chain] = "one-#{vm_id}-#{nic_id}",
vars[:chain_in] = "#{vars[:chain]}-i",
vars[:chain_out] = "#{vars[:chain]}-o"
if sg_id
vars[:set_sg_in] = "#{vars[:chain]}-#{sg_id}-i"
vars[:set_sg_out] = "#{vars[:chain]}-#{sg_id}-o"
end
vars
end
end
################################################################################
@ -461,23 +718,19 @@ end
class OpenNebulaSG < OpenNebulaNetwork
DRIVER = "sg"
XPATH_FILTER = "TEMPLATE/NIC[SECURITY_GROUPS]"
XPATH_FILTER = "TEMPLATE/NIC"
SECURITY_GROUP_CLASS = SecurityGroupIPTables
def initialize(vm, deploy_id = nil, hypervisor = nil)
super(vm, XPATH_FILTER, deploy_id, hypervisor)
@locking = true
@commands = Commands.new
get_security_group_rules
end
def activate
deactivate
lock
vm_id = @vm['ID']
security_group_rules = {}
def get_security_group_rules
rules = {}
@vm.vm_root.elements.each('TEMPLATE/SECURITY_GROUP_RULE') do |r|
security_group_rule = {}
@ -487,51 +740,35 @@ class OpenNebulaSG < OpenNebulaNetwork
end
id = security_group_rule[:security_group_id]
security_group_rules[id] = [] if security_group_rules[id].nil?
security_group_rules[id] << security_group_rule
rules[id] = [] if rules[id].nil?
rules[id] << security_group_rule
end
@security_group_rules = rules
end
def activate
deactivate
lock
# Global Bootstrap
SECURITY_GROUP_CLASS.global_bootstrap
# Process the rules
@vm.nics.each do |nic|
commands = Commands.new
next if nic[:security_groups].nil? \
&& nic[:filter_mac_spoofing].nil? \
&& nic[:filter_ip_spoofing].nil?
nic_id = nic[:nic_id]
chain = "one-#{vm_id}-#{nic_id}"
chain_in = "#{chain}-i"
chain_out = "#{chain}-o"
# create nic chains
commands.iptables("-N #{chain_in}") # inbound
commands.iptables("-N #{chain_out}") # outbound
# Send traffic to the NIC chains
commands.iptables("-A FORWARD -m physdev --physdev-out #{nic[:tap]} --physdev-is-bridged -j #{chain_in}")
commands.iptables("-A FORWARD -m physdev --physdev-in #{nic[:tap]} --physdev-is-bridged -j #{chain_out}")
# Related, Established
commands.iptables("-A #{chain_in} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT")
commands.iptables("-A #{chain_out} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT")
begin
commands.run!
rescue Exception => e
unlock
raise OpenNebulaSGError.new(:bootstrap, e)
end
SECURITY_GROUP_CLASS.nic_pre(@vm, nic)
sg_ids = nic[:security_groups].split(",")
sg_ids.each do |sg_id|
rules = security_group_rules[sg_id]
rules = @security_group_rules[sg_id]
sg = SECURITY_GROUP_CLASS.new(@vm, nic, sg_id, rules)
sg.bootstrap
sg.process_rules
begin
sg.process_rules
sg.run!
rescue Exception => e
unlock
@ -539,10 +776,7 @@ class OpenNebulaSG < OpenNebulaNetwork
end
end
commands.iptables("-A #{chain_in} -j DROP") # inbound
commands.iptables("-A #{chain_out} -j DROP") # outbound
commands.run!
SECURITY_GROUP_CLASS.nic_post(@vm, nic)
end
unlock
@ -551,58 +785,14 @@ class OpenNebulaSG < OpenNebulaNetwork
def deactivate
lock
vm_id = @vm['ID']
@vm.nics.each do |nic|
commands = Commands.new
nic_id = nic[:nic_id]
chain = "one-#{vm_id}-#{nic_id}"
chain_in = "#{chain}-i"
chain_out = "#{chain}-o"
# remove everything
begin
commands.iptables("-L FORWARD --line-numbers")
iptables_forwards = commands.run!
commands.iptables("-S")
iptables_s = commands.run!
commands.ipset("list -name")
ipset_list = commands.run!
iptables_forwards.lines.reverse.each do |line|
fields = line.split
if [chain_in, chain_out].include?(fields[1])
n = fields[0]
commands.iptables("-D FORWARD #{n}")
end
end
remove_chains = []
iptables_s.lines.each do |line|
if line.match(/^-N #{chain}/)
remove_chains << line.split[1]
end
end
remove_chains.each {|c| commands.iptables("-F #{c}") }
remove_chains.each {|c| commands.iptables("-X #{c}") }
ipset_list.lines.each do |line|
if line.match(/^#{chain}/)
set = line.strip
commands.ipset("destroy #{set}")
end
end
commands.run!
rescue Exception => e
raise OpenNebulaSGError.new(:deactivate, e)
begin
@vm.nics.each do |nic|
SECURITY_GROUP_CLASS.nic_deactivate(@vm, nic)
end
rescue Exception => e
raise OpenNebulaSGError.new(:deactivate, e)
ensure
unlock
end
unlock
end
end