Statistics
| Branch: | Tag: | Revision:

one / src / vnm_mad / remotes / lib / security_groups_iptables.rb @ d72c9d4f

History | View | Annotate | Download (20.7 KB)

1
# -------------------------------------------------------------------------- #
2
# Copyright 2002-2017, OpenNebula Project, OpenNebula Systems                #
3
#                                                                            #
4
# Licensed under the Apache License, Version 2.0 (the "License"); you may    #
5
# not use this file except in compliance with the License. You may obtain    #
6
# a copy of the License at                                                   #
7
#                                                                            #
8
# http://www.apache.org/licenses/LICENSE-2.0                                 #
9
#                                                                            #
10
# Unless required by applicable law or agreed to in writing, software        #
11
# distributed under the License is distributed on an "AS IS" BASIS,          #
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   #
13
# See the License for the specific language governing permissions and        #
14
# limitations under the License.                                             #
15
#--------------------------------------------------------------------------- #
16

    
17
module VNMMAD
18

    
19
# This module implements the SecurityGroup abstraction on top of iptables
20
module SGIPTables
21

    
22
    ############################################################################
23
    # A Rule implemented with the iptables/ipset Linux kernel facilities
24
    ############################################################################
25
    class RuleIPTables < VNMNetwork::Rule
26
        ########################################################################
27
        # Implementation of each rule type
28
        ########################################################################
29
        private
30

    
31
        # Implements the :protocol rule. Example:
32
        #   iptables -A one-3-0-i -p tcp -j RETURN
33
        def process_protocol(cmds, vars)
34
            chain = @rule_type == :inbound ? vars[:chain_in] : vars[:chain_out]
35

    
36
            if @protocol != :icmpv6
37
                cmds.add :iptables, "-A #{chain} -p #{@protocol} -j RETURN"
38
            end
39

    
40
            if @protocol != :icmp
41
                cmds.add :ip6tables, "-A #{chain} -p #{@protocol} -j RETURN"
42
            end
43
        end
44

    
45
        # Implements the :portrange rule. Example:
46
        #   iptables -A one-3-0-o -p udp -m multiport --dports 80,22 -j RETURN
47
        def process_portrange(cmds, vars)
48
            chain = @rule_type == :inbound ? vars[:chain_in] : vars[:chain_out]
49

    
50
            cmds.add :iptables, "-A #{chain} -p #{@protocol} -m multiport" \
51
                " --dports #{@range} -j RETURN"
52
            cmds.add :ip6tables, "-A #{chain} -p #{@protocol} -m multiport" \
53
                " --dports #{@range} -j RETURN"
54
        end
55

    
56
        # Implements the :icmp_type rule. Example:
57
        #   iptables -A one-3-0-o -p icmp --icmp-type 8 -j RETURN
58
        def process_icmp_type(cmds, vars)
59
            chain = @rule_type == :inbound ? vars[:chain_in] : vars[:chain_out]
60

    
61
            cmds.add :iptables, "-A #{chain} -p icmp --icmp-type #{@icmp_type}"\
62
                " -j RETURN"
63
        end
64

    
65
        # Implements the :icmpv6_type rule. Example:
66
        #   ip6tables -A one-3-0-o -p icmpv6 --icmpv6-type 128 -j RETURN
67
        def process_icmpv6_type(cmds, vars)
68
            chain = @rule_type == :inbound ? vars[:chain_in] : vars[:chain_out]
69

    
70
            cmds.add :ip6tables, "-A #{chain} -p icmpv6 --icmpv6-type "\
71
                "#{@icmpv6_type} -j RETURN"
72
        end
73

    
74
        # Implements the :net rule. Example:
75
        #   ipset create one-3-0-1-i-tcp-n-inet hash:net family inet
76
        #   iptables -A one-3-0-i -p tcp -m set --match-set \
77
        #       one-3-0-1-i-tcp-n-inet src -j RETURN
78
        #   ipset add -exist one-3-0-1-i-tcp-n-inet 10.0.0.0/24
79
        def process_net(cmds, vars)
80
            the_nets = net()
81

    
82
            return if the_nets.empty?
83

    
84
            sets = []
85

    
86
            the_nets.each do |n|
87
                if IPAddr.new(n).ipv6?
88
                    command = :ip6tables
89
                    family  = "inet6"
90
                else
91
                    command = :iptables
92
                    family  = "inet"
93
                end
94

    
95
                if @rule_type == :inbound
96
                    chain = vars[:chain_in]
97
                    set = "#{vars[:set_sg_in]}-#{@protocol}-n-#{family}"
98
                    dir = "src"
99
                else
100
                    chain = vars[:chain_out]
101
                    set = "#{vars[:set_sg_out]}-#{@protocol}-n-#{family}"
102
                    dir = "dst"
103
                end
104

    
105
                if !sets.include?(set)
106
                    cmds.add :ipset, "create #{set} hash:net family #{family}"
107
                    cmds.add command, "-A #{chain} -p #{@protocol} -m set" \
108
                        " --match-set #{set} #{dir} -j RETURN"
109

    
110
                    sets << set
111
                end
112

    
113
                cmds.add :ipset, "add -exist #{set} #{n}"
114
            end
115
        end
116

    
117
        # Implements the :net_portrange rule. Example:
118
        #   ipset create one-3-0-1-i-nr-inet hash:net,port family inet
119
        #   iptables -A one-3-0-i -m set --match-set one-3-0-1-i-nr-inet src,dst
120
        #        -j RETURN
121
        #   ipset add -exist one-3-0-1-i-nr-inet 10.0.0.0/24,tcp:80
122
        def process_net_portrange(cmds, vars)
123
            the_nets = net()
124

    
125
            return if the_nets.empty?
126

    
127
            sets = []
128

    
129
            the_nets.each do |n|
130
                if IPAddr.new(n).ipv6?
131
                    command = :ip6tables
132
                    family  = "inet6"
133
                else
134
                    command = :iptables
135
                    family  = "inet"
136
                end
137

    
138
                if @rule_type == :inbound
139
                    chain = vars[:chain_in]
140
                    set = "#{vars[:set_sg_in]}-nr-#{family}"
141
                    dir = "src,dst"
142
                else
143
                    chain = vars[:chain_out]
144
                    set = "#{vars[:set_sg_out]}-nr-#{family}"
145
                    dir = "dst,dst"
146
                end
147

    
148
                if !sets.include?(set)
149
                    cmds.add :ipset, "create #{set} hash:net,port family #{family}"
150
                    cmds.add command, "-A #{chain} -m set --match-set" \
151
                        " #{set} #{dir} -j RETURN"
152

    
153
                    sets << set
154
                end
155

    
156
                @range.split(",").each do |r|
157
                    r.gsub!(":","-")
158
                    net_range = "#{n},#{@protocol}:#{r}"
159

    
160
                    cmds.add :ipset, "add -exist #{set} #{net_range}"
161
                end
162
            end
163
        end
164

    
165
        # Implements the :net_icmp_type rule. Example:
166
        #   ipset create one-3-0-1-i-ni hash:net,port family inet
167
        #   iptables -A one-3-0-i -m set --match-set one-3-0-1-i-ni src,dst
168
        #       -j RETURN
169
        #   ipset add -exist one-3-0-1-i-ni 10.0.0.0/24,icmp:8/0
170
        def process_net_icmp_type(cmds, vars)
171
            if @rule_type == :inbound
172
                chain = vars[:chain_in]
173
                set = "#{vars[:set_sg_in]}-ni"
174
                dir = "src,dst"
175
            else
176
                chain = vars[:chain_out]
177
                set = "#{vars[:set_sg_out]}-ni"
178
                dir = "dst,dst"
179
            end
180

    
181
            cmds.add :ipset, "create #{set} hash:net,port family inet"
182
            cmds.add :iptables, "-A #{chain} -m set --match-set #{set} #{dir}"\
183
                " -j RETURN"
184

    
185
            net.each do |n|
186
                icmp_type_expand.each do |type_code|
187
                    cmds.add :ipset, "add -exist #{set} #{n},icmp:#{type_code}"
188
                end
189
            end
190
        end
191

    
192
        # Implements the :net_icmpv6_type rule. Example:
193
        #   ipset create one-3-0-1-i-ni6 hash:net,port family inet6
194
        #   ip6tables -A one-3-0-i -m set --match-set one-3-0-1-i-ni6 src,dst
195
        #       -j RETURN
196
        #   ipset add -exist one-3-0-1-i-ni6 10.0.0.0/24,icmpv6:128/0
197
        def process_net_icmpv6_type(cmds, vars)
198
            if @rule_type == :inbound
199
                chain = vars[:chain_in]
200
                set = "#{vars[:set_sg_in]}-ni6"
201
                dir = "src,dst"
202
            else
203
                chain = vars[:chain_out]
204
                set = "#{vars[:set_sg_out]}-ni6"
205
                dir = "dst,dst"
206
            end
207

    
208
            cmds.add :ipset, "create #{set} hash:net,port family inet6"
209
            cmds.add :ip6tables, "-A #{chain} -m set --match-set #{set} #{dir}"\
210
                " -j RETURN"
211

    
212
            net.each do |n|
213
                icmpv6_type_expand.each do |type_code|
214
                    cmds.add :ipset, "add -exist #{set} #{n},icmpv6:#{type_code}"
215
                end
216
            end
217
        end
218
    end
219

    
220
    ############################################################################
221
    # This class represents a SecurityGroup implemented with iptables/ipset
222
    # Kernel facilities.
223
    ############################################################################
224
    class SecurityGroupIPTables < VNMNetwork::SecurityGroup
225
        def initialize(vm, nic, sg_id, rules)
226
            super
227

    
228
            @vars = SGIPTables.vars(@vm, @nic, @sg_id)
229
        end
230

    
231
        def new_rule(rule)
232
            RuleIPTables.new(rule)
233
        end
234
    end
235

    
236
    ############################################################################
237
    # Methods to configure the hypervisor iptables rules. All the rules are
238
    # added to the GLOBAL_CHAIN chain. By default this chain is "opennebula"
239
    ############################################################################
240

    
241
    GLOBAL_CHAIN = "opennebula"
242

    
243
    # Get information from the current iptables rules and chains
244
    #   @return [Hash] with the following keys:
245
    #     - :iptables_forwards
246
    #     - :iptables_s
247
    #     - :ipset_list
248
    def self.info
249
        commands = VNMNetwork::Commands.new
250

    
251
        commands.add :iptables, "-S"
252
        iptables_s = commands.run!
253

    
254
        commands.add :ip6tables, "-S"
255
        ip6tables_s = commands.run!
256

    
257
        iptables_forwards = ""
258
        ip6tables_forwards = ""
259

    
260
        if iptables_s.match(/^-N #{GLOBAL_CHAIN}$/)
261
            commands.add :iptables, "-L #{GLOBAL_CHAIN} --line-numbers"
262
            iptables_forwards = commands.run!
263
        end
264

    
265
        if ip6tables_s.match(/^-N #{GLOBAL_CHAIN}$/)
266
            commands.add :ip6tables, "-L #{GLOBAL_CHAIN} --line-numbers"
267
            ip6tables_forwards = commands.run!
268
        end
269

    
270
        commands.add :ipset, "list -name"
271
        ipset_list = commands.run!
272

    
273
        {
274
            :iptables_forwards => iptables_forwards,
275
            :iptables_s => iptables_s,
276
            :ip6tables_forwards => ip6tables_forwards,
277
            :ip6tables_s => ip6tables_s,
278
            :ipset_list => ipset_list
279
        }
280
    end
281

    
282
    # Bootstrap the OpenNebula chains and rules. This method:
283
    #   1.- Creates the GLOBAL_CHAIN chain
284
    #   2.- Forwards the bridge traffic to the GLOBAL_CHAIN
285
    #   3.- By default ACCEPT all traffic
286
    def self.global_bootstrap
287
        info = SGIPTables.info
288

    
289
        commands = VNMNetwork::Commands.new
290

    
291
        if !info[:iptables_s].split("\n").include?("-N #{GLOBAL_CHAIN}")
292
            commands.add :iptables, "-N #{GLOBAL_CHAIN}"
293
            commands.add :iptables, "-A FORWARD -m physdev "\
294
                "--physdev-is-bridged -j #{GLOBAL_CHAIN}"
295
            commands.add :iptables, "-A #{GLOBAL_CHAIN} -j ACCEPT"
296
        end
297

    
298
        if !info[:ip6tables_s].split("\n").include?("-N #{GLOBAL_CHAIN}")
299
            commands.add :ip6tables, "-N #{GLOBAL_CHAIN}"
300
            commands.add :ip6tables, "-A FORWARD -m physdev "\
301
                "--physdev-is-bridged -j #{GLOBAL_CHAIN}"
302
            commands.add :ip6tables, "-A #{GLOBAL_CHAIN} -j ACCEPT"
303
        end
304

    
305
        commands.run! if !commands.empty?
306
    end
307

    
308
    # Returns the base chain and ipset names for the VM
309
    #   @param vm  [VM] the virtual machine
310
    #   @param nic [Nic] of the VM
311
    #   @param sg_id [Fixnum] ID of the SecurityGroup if any
312
    #
313
    #   @return [Hash] with the :chain, :chain_in, :chain_out chain names, and
314
    #   :set_sg_in and :set_seg_out ipset names.
315
    def self.vars(vm, nic, sg_id = nil)
316
        vm_id  = vm['ID']
317
        nic_id = nic[:nic_id]
318

    
319
        vars = {}
320

    
321
        vars[:vm_id]     = vm_id,
322
        vars[:nic_id]    = nic_id,
323
        vars[:chain]     = "one-#{vm_id}-#{nic_id}",
324
        vars[:chain_in]  = "#{vars[:chain]}-i",
325
        vars[:chain_out] = "#{vars[:chain]}-o"
326

    
327
        if sg_id
328
            vars[:set_sg_in]  = "#{vars[:chain]}-#{sg_id}-i"
329
            vars[:set_sg_out] = "#{vars[:chain]}-#{sg_id}-o"
330
        end
331

    
332
        vars
333
    end
334

    
335
    #  Bootstrap NIC rules for the interface. It creates the :chain_in and
336
    #  :chain_out and sets up FORWARD rules to these chains for inbound and
337
    #  outbound traffic.
338
    #
339
    #  This method also sets mac_spoofing, and ip_spoofing rules
340
    #
341
    #  Example, for VM 3 and NIC 0
342
    #   iptables -N one-3-0-i
343
    #   iptables -N one-3-0-o
344
    #   iptables -I opennebula -m physdev --physdev-out vnet0
345
    #       --physdev-is-bridged -j one-3-0-i"
346
    #   iptables -I opennebula -m physdev --physdev-in  vnet0
347
    #       --physdev-is-bridged -j one-3-0-o"
348
    #   iptables -A one-3-0-i -m state --state ESTABLISHED,RELATED -j ACCEPT
349
    #   iptables -A one-3-0-o -m state --state ESTABLISHED,RELATED -j ACCEPT
350
    #
351
    #   Mac spoofing (no output traffic from a different MAC)
352
    #   iptables -A one-3-0-o -m mac ! --mac-source 02:00:00:00:00:01 -j DROP
353
    #
354
    #   IP spoofing
355
    #   iptables -A one-3-0-o ! --source 10.0.0.1 -j DROP
356
    def self.nic_pre(vm, nic)
357
        commands = VNMNetwork::Commands.new
358

    
359
        vars = SGIPTables.vars(vm, nic)
360

    
361
        chain     = vars[:chain]
362
        chain_in  = vars[:chain_in]
363
        chain_out = vars[:chain_out]
364

    
365
        # create chains
366
        commands.add :iptables, "-N #{chain_in}"  # inbound
367
        commands.add :iptables, "-N #{chain_out}" # outbound
368
        commands.add :ip6tables, "-N #{chain_in}"  # inbound
369
        commands.add :ip6tables, "-N #{chain_out}" # outbound
370

    
371
        # Send traffic to the NIC chains
372
        commands.add :iptables, "-I #{GLOBAL_CHAIN} -m physdev "\
373
            "--physdev-out #{nic[:tap]} --physdev-is-bridged -j #{chain_in}"
374
        commands.add :iptables, "-I #{GLOBAL_CHAIN} -m physdev "\
375
            "--physdev-in  #{nic[:tap]} --physdev-is-bridged -j #{chain_out}"
376

    
377
        commands.add :ip6tables, "-I #{GLOBAL_CHAIN} -m physdev "\
378
            "--physdev-out #{nic[:tap]} --physdev-is-bridged -j #{chain_in}"
379
        commands.add :ip6tables, "-I #{GLOBAL_CHAIN} -m physdev "\
380
            "--physdev-in  #{nic[:tap]} --physdev-is-bridged -j #{chain_out}"
381

    
382
        # ICMPv6 Neighbor Discovery Protocol (ARP replacement for IPv6)
383
        ## Allow routers to send router advertisements
384
        commands.add :ip6tables, "-A #{chain_in} -p icmpv6 --icmpv6-type 134 "\
385
            "-j ACCEPT"
386

    
387
        ## Allow neighbor solicitations to reach the host
388
        commands.add :ip6tables, "-A #{chain_in} -p icmpv6 --icmpv6-type 135 "\
389
            "-j ACCEPT"
390

    
391
        ## Allow neighbor solicitations replies to reach the host
392
        commands.add :ip6tables, "-A #{chain_in} -p icmpv6 --icmpv6-type 136 "\
393
            "-j ACCEPT"
394

    
395
        ## Allow routers to send Redirect messages
396
        commands.add :ip6tables, "-A #{chain_in} -p icmpv6 --icmpv6-type 137 "\
397
            "-j ACCEPT"
398

    
399
        ## Allow the host to send a router solicitation
400
        commands.add :ip6tables, "-A #{chain_out} -p icmpv6 --icmpv6-type 133 "\
401
            "-j ACCEPT"
402

    
403
        ## Allow the host to send neighbor solicitation requests
404
        commands.add :ip6tables, "-A #{chain_out} -p icmpv6 --icmpv6-type 135 "\
405
            "-j ACCEPT"
406

    
407
        ## Allow the host to send neighbor solicitation replies
408
        commands.add :ip6tables, "-A #{chain_out} -p icmpv6 --icmpv6-type 136 "\
409
            "-j ACCEPT"
410

    
411
        # Mac-spofing
412
        if nic[:filter_mac_spoofing] == "YES"
413
            commands.add :iptables, "-A #{chain_out} -m mac ! "\
414
                "--mac-source #{nic[:mac]} -j DROP"
415
            commands.add :ip6tables, "-A #{chain_out} -m mac ! "\
416
                "--mac-source #{nic[:mac]} -j DROP"
417
        end
418

    
419
        # IP-spofing
420
        if nic[:filter_ip_spoofing] == "YES"
421
            ipv4s = Array.new
422

    
423
            [:ip, :vrouter_ip].each do |key|
424
                ipv4s << nic[key] if !nic[key].nil? && !nic[key].empty?
425
            end
426

    
427
            if !ipv4s.empty?
428
                #bootp
429
                commands.add :iptables, "-A #{chain_out} -p udp "\
430
                    "--source 0.0.0.0/32 --sport 68 --destination "\
431
                    "255.255.255.255/32 --dport 67 -j ACCEPT"
432

    
433
                set = "#{vars[:chain]}-ip-spoofing"
434

    
435
                commands.add :ipset, "create #{set} hash:ip family inet"
436

    
437
                ipv4s.each do |ip|
438
                    commands.add :ipset, "add -exist #{set} #{ip}"
439
                end
440

    
441
                commands.add :iptables, "-A #{chain_out} -m set ! "\
442
                    "--match-set #{set} src -j DROP"
443
            else # If there are no IPv4 addresses allowed, block all
444
                commands.add :iptables, "-A #{chain_out} --source 0.0.0.0/0 "\
445
                    "-j DROP"
446
            end
447

    
448
            ipv6s = Array.new
449

    
450
            [:ip6_global, :ip6_link, :ip6_ula].each do |key|
451
                ipv6s << nic[key] if !nic[key].nil? && !nic[key].empty?
452
            end
453

    
454
            if !ipv6s.empty?
455
                set = "#{vars[:chain]}-ip6-spoofing"
456

    
457
                commands.add :ipset, "create #{set} hash:ip family inet6"
458

    
459
                ipv6s.each do |ip|
460
                    commands.add :ipset, "add -exist #{set} #{ip}"
461
                end
462

    
463
                commands.add :ip6tables, "-A #{chain_out} -m set ! "\
464
                    "--match-set #{set} src -j DROP"
465
            else # If there are no IPv6 addresses allowed, block all
466
                commands.add :ip6tables, "-A #{chain_out} --source ::/0 -j DROP"
467
            end
468
        end
469

    
470
        # Related, Established
471
        commands.add :iptables, "-A #{chain_in} -m state"\
472
            " --state ESTABLISHED,RELATED -j ACCEPT"
473
        commands.add :iptables, "-A #{chain_out} -m state"\
474
            " --state ESTABLISHED,RELATED -j ACCEPT"
475
        commands.add :ip6tables, "-A #{chain_in} -m state"\
476
            " --state ESTABLISHED,RELATED -j ACCEPT"
477
        commands.add :ip6tables, "-A #{chain_out} -m state"\
478
            " --state ESTABLISHED,RELATED -j ACCEPT"
479

    
480
        commands.run!
481
    end
482

    
483
    # Sets the default policy to DROP for the NIC rules. Example
484
    #   iptables -A one-3-0-i -j DROP
485
    #   iptables -A one-3-0-o -j DROP
486
    def self.nic_post(vm, nic)
487
        vars      = SGIPTables.vars(vm, nic)
488
        chain_in  = vars[:chain_in]
489
        chain_out = vars[:chain_out]
490

    
491
        commands = VNMNetwork::Commands.new
492

    
493
        commands.add :iptables, "-A #{chain_in}  -j DROP"
494
        commands.add :iptables, "-A #{chain_out} -j DROP"
495

    
496
        commands.add :ip6tables, "-A #{chain_in}  -j DROP"
497
        commands.add :ip6tables, "-A #{chain_out} -j DROP"
498

    
499
        commands.run!
500
    end
501

    
502
    # Removes all the rules associated to a VM and NIC
503
    def self.nic_deactivate(vm, nic)
504
        vars      = SGIPTables.vars(vm, nic)
505
        chain     = vars[:chain]
506
        chain_in  = vars[:chain_in]
507
        chain_out = vars[:chain_out]
508

    
509
        info = self.info
510

    
511
        iptables_forwards  = info[:iptables_forwards]
512
        iptables_s         = info[:iptables_s]
513

    
514
        ip6tables_forwards = info[:ip6tables_forwards]
515
        ip6tables_s        = info[:ip6tables_s]
516

    
517
        ipset_list = info[:ipset_list]
518

    
519
        commands = VNMNetwork::Commands.new
520

    
521
        iptables_forwards.lines.reverse_each do |line|
522
            fields = line.split
523
            if [chain_in, chain_out].include?(fields[1])
524
                n = fields[0]
525
                commands.add :iptables, "-D #{GLOBAL_CHAIN} #{n}"
526
            end
527
        end
528

    
529
        ip6tables_forwards.lines.reverse_each do |line|
530
            fields = line.split
531
            if [chain_in, chain_out].include?(fields[1])
532
                n = fields[0]
533
                commands.add :ip6tables, "-D #{GLOBAL_CHAIN} #{n}"
534
            end
535
        end
536

    
537
        remove_chains = []
538
        iptables_s.lines.each do |line|
539
            if line.match(/^-N #{chain}(-|$)/)
540
                remove_chains << line.split[1]
541
            end
542
        end
543
        remove_chains.each {|c| commands.add :iptables, "-F #{c}" }
544
        remove_chains.each {|c| commands.add :iptables, "-X #{c}" }
545

    
546
        remove_chains_6 = []
547
        ip6tables_s.lines.each do |line|
548
            if line.match(/^-N #{chain}(-|$)/)
549
                remove_chains_6 << line.split[1]
550
            end
551
        end
552
        remove_chains_6.each {|c| commands.add :ip6tables, "-F #{c}" }
553
        remove_chains_6.each {|c| commands.add :ip6tables, "-X #{c}" }
554

    
555
        ipset_list.lines.each do |line|
556
            if line.match(/^#{chain}(-|$)/)
557
                set = line.strip
558
                commands.add :ipset, "destroy #{set}"
559
            end
560
        end
561

    
562
        commands.run!
563
    end
564
end
565

    
566
end