diff --git a/src/sunstone/public/js/plugins/secgroups-tab.js b/src/sunstone/public/js/plugins/secgroups-tab.js index 546571d473..2d90832ac4 100644 --- a/src/sunstone/public/js/plugins/secgroups-tab.js +++ b/src/sunstone/public/js/plugins/secgroups-tab.js @@ -389,36 +389,19 @@ var create_security_group_wizard_html = \ \ \ \ @@ -918,7 +901,7 @@ function updateSecurityGroupInfo(request,security_group){ } function insert_sg_rules_table(sg){ - var html = + var html = '\ \ \ diff --git a/src/vnm_mad/remotes/security_groups/SecurityGroups.rb b/src/vnm_mad/remotes/security_groups/SecurityGroups.rb index 232927b07e..ec0e7444a3 100644 --- a/src/vnm_mad/remotes/security_groups/SecurityGroups.rb +++ b/src/vnm_mad/remotes/security_groups/SecurityGroups.rb @@ -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 '' or '/', where both + # '' and '' are integers. This class has a helper method + # tgat expands '' into all the '/' 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