summaryrefslogtreecommitdiff
path: root/neutron/agent/linux/iptables_firewall.py
blob: 9212ff0bb254b3acda171efc6594c3a06e64aadf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
# Copyright 2012, Nachi Ueno, NTT MCL, 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.

import collections

import netaddr
from neutron_lib import constants
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import netutils
import six

from neutron._i18n import _, _LI
from neutron.agent import firewall
from neutron.agent.linux import ip_conntrack
from neutron.agent.linux import ipset_manager
from neutron.agent.linux import iptables_comments as ic
from neutron.agent.linux import iptables_manager
from neutron.agent.linux import utils
from neutron.common import constants as n_const
from neutron.common import ipv6_utils
from neutron.common import utils as c_utils


LOG = logging.getLogger(__name__)
SG_CHAIN = 'sg-chain'
SPOOF_FILTER = 'spoof-filter'
CHAIN_NAME_PREFIX = {firewall.INGRESS_DIRECTION: 'i',
                     firewall.EGRESS_DIRECTION: 'o',
                     SPOOF_FILTER: 's'}
IPSET_DIRECTION = {firewall.INGRESS_DIRECTION: 'src',
                   firewall.EGRESS_DIRECTION: 'dst'}
LINUX_DEV_PREFIX_LEN = n_const.LINUX_DEV_PREFIX_LEN
LINUX_DEV_LEN = n_const.LINUX_DEV_LEN
MAX_CONNTRACK_ZONES = ip_conntrack.MAX_CONNTRACK_ZONES
comment_rule = iptables_manager.comment_rule


def get_hybrid_port_name(port_name):
    return (constants.TAP_DEVICE_PREFIX + port_name)[:n_const.LINUX_DEV_LEN]


class mac_iptables(netaddr.mac_eui48):
    """mac format class for netaddr to match iptables representation."""
    word_sep = ':'


class IptablesFirewallDriver(firewall.FirewallDriver):
    """Driver which enforces security groups through iptables rules."""
    IPTABLES_DIRECTION = {firewall.INGRESS_DIRECTION: 'physdev-out',
                          firewall.EGRESS_DIRECTION: 'physdev-in'}
    CONNTRACK_ZONE_PER_PORT = False

    def __init__(self, namespace=None):
        self.iptables = iptables_manager.IptablesManager(
            state_less=True,
            use_ipv6=ipv6_utils.is_enabled(),
            namespace=namespace)
        # TODO(majopela, shihanzhang): refactor out ipset to a separate
        # driver composed over this one
        self.ipset = ipset_manager.IpsetManager(namespace=namespace)
        # list of port which has security group
        self.filtered_ports = {}
        self.unfiltered_ports = {}
        self.ipconntrack = ip_conntrack.get_conntrack(
            self.iptables.get_rules_for_table, self.filtered_ports,
            self.unfiltered_ports, namespace=namespace,
            zone_per_port=self.CONNTRACK_ZONE_PER_PORT)
        self._add_fallback_chain_v4v6()
        self._defer_apply = False
        self._pre_defer_filtered_ports = None
        self._pre_defer_unfiltered_ports = None
        # List of security group rules for ports residing on this host
        self.sg_rules = {}
        self.pre_sg_rules = None
        # List of security group member ips for ports residing on this host
        self.sg_members = collections.defaultdict(
            lambda: collections.defaultdict(list))
        self.pre_sg_members = None
        self.enable_ipset = cfg.CONF.SECURITYGROUP.enable_ipset
        self._enabled_netfilter_for_bridges = False
        self.updated_rule_sg_ids = set()
        self.updated_sg_members = set()
        self.devices_with_updated_sg_members = collections.defaultdict(list)

    def _enable_netfilter_for_bridges(self):
        # we only need to set these values once, but it has to be when
        # we create a bridge; before that the bridge module might not
        # be loaded and the proc values aren't there.
        if self._enabled_netfilter_for_bridges:
            return
        else:
            self._enabled_netfilter_for_bridges = True

        # These proc values ensure that netfilter is enabled on
        # bridges; essential for enforcing security groups rules with
        # OVS Hybrid.  Distributions can differ on whether this is
        # enabled by default or not (Ubuntu - yes, Redhat - no, for
        # example).
        LOG.debug("Enabling netfilter for bridges")
        entries = utils.execute(['sysctl', '-N', 'net.bridge'],
                                run_as_root=True).splitlines()
        for proto in ('arp', 'ip', 'ip6'):
            knob = 'net.bridge.bridge-nf-call-%stables' % proto
            if 'net.bridge.bridge-nf-call-%stables' % proto not in entries:
                raise SystemExit(
                    _("sysctl value %s not present on this system.") % knob)
            enabled = utils.execute(['sysctl', '-b', knob])
            if enabled != '1':
                utils.execute(
                    ['sysctl', '-w', '%s=1' % knob], run_as_root=True)

    @property
    def ports(self):
        return dict(self.filtered_ports, **self.unfiltered_ports)

    def _update_remote_security_group_members(self, sec_group_ids):
        for sg_id in sec_group_ids:
            for device in self.filtered_ports.values():
                if sg_id in device.get('security_group_source_groups', []):
                    self.devices_with_updated_sg_members[sg_id].append(device)

    def security_group_updated(self, action_type, sec_group_ids,
                               device_ids=None):
        device_ids = device_ids or []
        if action_type == 'sg_rule':
            self.updated_rule_sg_ids.update(sec_group_ids)
        elif action_type == 'sg_member':
            if device_ids:
                self.updated_sg_members.update(device_ids)
            else:
                self._update_remote_security_group_members(sec_group_ids)

    def update_security_group_rules(self, sg_id, sg_rules):
        LOG.debug("Update rules of security group (%s)", sg_id)
        self.sg_rules[sg_id] = sg_rules

    def update_security_group_members(self, sg_id, sg_members):
        LOG.debug("Update members of security group (%s)", sg_id)
        self.sg_members[sg_id] = collections.defaultdict(list, sg_members)
        if self.enable_ipset:
            self._update_ipset_members(sg_id, sg_members)

    def _update_ipset_members(self, sg_id, sg_members):
        devices = self.devices_with_updated_sg_members.pop(sg_id, None)
        for ip_version, current_ips in sg_members.items():
            add_ips, del_ips = self.ipset.set_members(
                sg_id, ip_version, current_ips)
            if devices and del_ips:
                # remove prefix from del_ips
                ips = [str(netaddr.IPNetwork(del_ip).ip) for del_ip in del_ips]
                self.ipconntrack.delete_conntrack_state_by_remote_ips(
                    devices, ip_version, ips)

    def _set_ports(self, port):
        if not firewall.port_sec_enabled(port):
            self.unfiltered_ports[port['device']] = port
            self.filtered_ports.pop(port['device'], None)
        else:
            self.filtered_ports[port['device']] = port
            self.unfiltered_ports.pop(port['device'], None)

    def _unset_ports(self, port):
        self.unfiltered_ports.pop(port['device'], None)
        self.filtered_ports.pop(port['device'], None)

    def _remove_conntrack_entries_from_port_deleted(self, port):
        device_info = self.filtered_ports.get(port['device'])
        if not device_info:
            return
        for ethertype in [constants.IPv4, constants.IPv6]:
            self.ipconntrack.delete_conntrack_state_by_remote_ips(
                [device_info], ethertype, set())

    def prepare_port_filter(self, port):
        LOG.debug("Preparing device (%s) filter", port['device'])
        self._set_ports(port)
        self._enable_netfilter_for_bridges()
        # each security group has it own chains
        self._setup_chains()
        return self.iptables.apply()

    def update_port_filter(self, port):
        LOG.debug("Updating device (%s) filter", port['device'])
        if port['device'] not in self.ports:
            LOG.info(_LI('Attempted to update port filter which is not '
                         'filtered %s'), port['device'])
            return
        self._remove_chains()
        self._set_ports(port)
        self._setup_chains()
        return self.iptables.apply()

    def remove_port_filter(self, port):
        LOG.debug("Removing device (%s) filter", port['device'])
        if port['device'] not in self.ports:
            LOG.info(_LI('Attempted to remove port filter which is not '
                         'filtered %r'), port)
            return
        self._remove_chains()
        self._remove_conntrack_entries_from_port_deleted(port)
        self._unset_ports(port)
        self._setup_chains()
        return self.iptables.apply()

    def _add_accept_rule_port_sec(self, port, direction):
        self._update_port_sec_rules(port, direction, add=True)

    def _remove_rule_port_sec(self, port, direction):
        self._update_port_sec_rules(port, direction, add=False)

    def _remove_rule_from_chain_v4v6(self, chain_name, ipv4_rules, ipv6_rules):
        for rule in ipv4_rules:
            self.iptables.ipv4['filter'].remove_rule(chain_name, rule)

        for rule in ipv6_rules:
            self.iptables.ipv6['filter'].remove_rule(chain_name, rule)

    def _setup_chains(self):
        """Setup ingress and egress chain for a port."""
        if not self._defer_apply:
            self._setup_chains_apply(self.filtered_ports,
                                     self.unfiltered_ports)

    def _setup_chains_apply(self, ports, unfiltered_ports):
        self._add_chain_by_name_v4v6(SG_CHAIN)
        # sort by port so we always do this deterministically between
        # agent restarts and don't cause unnecessary rule differences
        for pname in sorted(ports):
            port = ports[pname]
            self._add_conntrack_jump(port)
            self._setup_chain(port, firewall.INGRESS_DIRECTION)
            self._setup_chain(port, firewall.EGRESS_DIRECTION)
        self.iptables.ipv4['filter'].add_rule(SG_CHAIN, '-j ACCEPT')
        self.iptables.ipv6['filter'].add_rule(SG_CHAIN, '-j ACCEPT')

        for port in unfiltered_ports.values():
            self._add_accept_rule_port_sec(port, firewall.INGRESS_DIRECTION)
            self._add_accept_rule_port_sec(port, firewall.EGRESS_DIRECTION)

    def _remove_chains(self):
        """Remove ingress and egress chain for a port."""
        if not self._defer_apply:
            self._remove_chains_apply(self.filtered_ports,
                                      self.unfiltered_ports)

    def _remove_chains_apply(self, ports, unfiltered_ports):
        for port in ports.values():
            self._remove_chain(port, firewall.INGRESS_DIRECTION)
            self._remove_chain(port, firewall.EGRESS_DIRECTION)
            self._remove_chain(port, SPOOF_FILTER)
            self._remove_conntrack_jump(port)
        for port in unfiltered_ports.values():
            self._remove_rule_port_sec(port, firewall.INGRESS_DIRECTION)
            self._remove_rule_port_sec(port, firewall.EGRESS_DIRECTION)
        self._remove_chain_by_name_v4v6(SG_CHAIN)

    def _setup_chain(self, port, DIRECTION):
        self._add_chain(port, DIRECTION)
        self._add_rules_by_security_group(port, DIRECTION)

    def _remove_chain(self, port, DIRECTION):
        chain_name = self._port_chain_name(port, DIRECTION)
        self._remove_chain_by_name_v4v6(chain_name)

    def _add_fallback_chain_v4v6(self):
        self.iptables.ipv4['filter'].add_chain('sg-fallback')
        self.iptables.ipv4['filter'].add_rule('sg-fallback', '-j DROP',
                                              comment=ic.UNMATCH_DROP)
        self.iptables.ipv6['filter'].add_chain('sg-fallback')
        self.iptables.ipv6['filter'].add_rule('sg-fallback', '-j DROP',
                                              comment=ic.UNMATCH_DROP)

    def _add_raw_chain(self, chain_name):
        self.iptables.ipv4['raw'].add_chain(chain_name)
        self.iptables.ipv6['raw'].add_chain(chain_name)

    def _add_chain_by_name_v4v6(self, chain_name):
        self.iptables.ipv4['filter'].add_chain(chain_name)
        self.iptables.ipv6['filter'].add_chain(chain_name)

    def _remove_raw_chain(self, chain_name):
        self.iptables.ipv4['raw'].remove_chain(chain_name)
        self.iptables.ipv6['raw'].remove_chain(chain_name)

    def _remove_chain_by_name_v4v6(self, chain_name):
        self.iptables.ipv4['filter'].remove_chain(chain_name)
        self.iptables.ipv6['filter'].remove_chain(chain_name)

    def _add_rules_to_chain_v4v6(self, chain_name, ipv4_rules, ipv6_rules,
                                 comment=None):
        for rule in ipv4_rules:
            self.iptables.ipv4['filter'].add_rule(chain_name, rule,
                                                  comment=comment)

        for rule in ipv6_rules:
            self.iptables.ipv6['filter'].add_rule(chain_name, rule,
                                                  comment=comment)

    def _get_device_name(self, port):
        return port['device']

    def _update_port_sec_rules(self, port, direction, add=False):
        # add/remove rules in FORWARD and INPUT chain
        device = self._get_device_name(port)

        jump_rule = ['-m physdev --%s %s --physdev-is-bridged '
                     '-j ACCEPT' % (self.IPTABLES_DIRECTION[direction],
                                    device)]
        if add:
            self._add_rules_to_chain_v4v6(
                'FORWARD', jump_rule, jump_rule, comment=ic.PORT_SEC_ACCEPT)
        else:
            self._remove_rule_from_chain_v4v6('FORWARD', jump_rule, jump_rule)

        if direction == firewall.EGRESS_DIRECTION:
            jump_rule = ['-m physdev --%s %s --physdev-is-bridged '
                         '-j ACCEPT' % (self.IPTABLES_DIRECTION[direction],
                                        device)]
            if add:
                self._add_rules_to_chain_v4v6('INPUT', jump_rule, jump_rule,
                                              comment=ic.PORT_SEC_ACCEPT)
            else:
                self._remove_rule_from_chain_v4v6(
                    'INPUT', jump_rule, jump_rule)

    def _add_chain(self, port, direction):
        chain_name = self._port_chain_name(port, direction)
        self._add_chain_by_name_v4v6(chain_name)

        # Note(nati) jump to the security group chain (SG_CHAIN)
        # This is needed because the packet may much two rule in port
        # if the two port is in the same host
        # We accept the packet at the end of SG_CHAIN.

        # jump to the security group chain
        device = self._get_device_name(port)
        jump_rule = ['-m physdev --%s %s --physdev-is-bridged '
                     '-j $%s' % (self.IPTABLES_DIRECTION[direction],
                                 device,
                                 SG_CHAIN)]
        self._add_rules_to_chain_v4v6('FORWARD', jump_rule, jump_rule,
                                      comment=ic.VM_INT_SG)

        # jump to the chain based on the device
        jump_rule = ['-m physdev --%s %s --physdev-is-bridged '
                     '-j $%s' % (self.IPTABLES_DIRECTION[direction],
                                 device,
                                 chain_name)]
        self._add_rules_to_chain_v4v6(SG_CHAIN, jump_rule, jump_rule,
                                      comment=ic.SG_TO_VM_SG)

        if direction == firewall.EGRESS_DIRECTION:
            self._add_rules_to_chain_v4v6('INPUT', jump_rule, jump_rule,
                                          comment=ic.INPUT_TO_SG)

    def _get_br_device_name(self, port):
        return ('brq' + port['network_id'])[:n_const.LINUX_DEV_LEN]

    def _get_jump_rules(self, port):
        zone = self.ipconntrack.get_device_zone(port)
        br_dev = self._get_br_device_name(port)
        port_dev = self._get_device_name(port)
        # match by interface for bridge input
        match_interface = '-i %s'
        match_physdev = '-m physdev --physdev-in %s'
        # comment to prevent duplicate warnings for different devices using
        # same bridge. truncate start to remove prefixes
        comment = '-m comment --comment "Set zone for %s"' % port['device'][4:]
        rules = []
        for dev, match in ((br_dev, match_physdev), (br_dev, match_interface),
                           (port_dev, match_physdev)):
            match = match % dev
            rule = '%s %s -j CT --zone %s' % (match, comment, zone)
            rules.append(rule)
        return rules

    def _add_conntrack_jump(self, port):
        for jump_rule in self._get_jump_rules(port):
            self._add_raw_rule('PREROUTING', jump_rule)

    def _remove_conntrack_jump(self, port):
        for jump_rule in self._get_jump_rules(port):
            self._remove_raw_rule('PREROUTING', jump_rule)

    def _add_raw_rule(self, chain, rule, comment=None):
        self.iptables.ipv4['raw'].add_rule(chain, rule, comment=comment)
        self.iptables.ipv6['raw'].add_rule(chain, rule, comment=comment)

    def _remove_raw_rule(self, chain, rule):
        self.iptables.ipv4['raw'].remove_rule(chain, rule)
        self.iptables.ipv6['raw'].remove_rule(chain, rule)

    def _split_sgr_by_ethertype(self, security_group_rules):
        ipv4_sg_rules = []
        ipv6_sg_rules = []
        for rule in security_group_rules:
            if rule.get('ethertype') == constants.IPv4:
                ipv4_sg_rules.append(rule)
            elif rule.get('ethertype') == constants.IPv6:
                if rule.get('protocol') == 'icmp':
                    rule['protocol'] = 'ipv6-icmp'
                ipv6_sg_rules.append(rule)
        return ipv4_sg_rules, ipv6_sg_rules

    def _select_sgr_by_direction(self, port, direction):
        return [rule
                for rule in port.get('security_group_rules', [])
                if rule['direction'] == direction]

    def _setup_spoof_filter_chain(self, port, table, mac_ip_pairs, rules):
        if mac_ip_pairs:
            chain_name = self._port_chain_name(port, SPOOF_FILTER)
            table.add_chain(chain_name)
            for mac, ip in mac_ip_pairs:
                if ip is None:
                    # If fixed_ips is [] this rule will be added to the end
                    # of the list after the allowed_address_pair rules.
                    table.add_rule(chain_name,
                                   '-m mac --mac-source %s -j RETURN'
                                   % mac.upper(), comment=ic.PAIR_ALLOW)
                else:
                    # we need to convert it into a prefix to match iptables
                    ip = c_utils.ip_to_cidr(ip)
                    table.add_rule(chain_name,
                                   '-s %s -m mac --mac-source %s -j RETURN'
                                   % (ip, mac.upper()), comment=ic.PAIR_ALLOW)
            table.add_rule(chain_name, '-j DROP', comment=ic.PAIR_DROP)
            rules.append('-j $%s' % chain_name)

    def _build_ipv4v6_mac_ip_list(self, mac, ip_address, mac_ipv4_pairs,
                                  mac_ipv6_pairs):
        mac = str(netaddr.EUI(mac, dialect=mac_iptables))
        if netaddr.IPNetwork(ip_address).version == 4:
            mac_ipv4_pairs.append((mac, ip_address))
        else:
            mac_ipv6_pairs.append((mac, ip_address))
            lla = str(netutils.get_ipv6_addr_by_EUI64(
                    constants.IPv6_LLA_PREFIX, mac))
            if (mac, lla) not in mac_ipv6_pairs:
                # only add once so we don't generate duplicate rules
                mac_ipv6_pairs.append((mac, lla))

    def _spoofing_rule(self, port, ipv4_rules, ipv6_rules):
        # Fixed rules for traffic sourced from unspecified addresses: 0.0.0.0
        # and ::
        # Allow dhcp client discovery and request
        ipv4_rules += [comment_rule('-s 0.0.0.0/32 -d 255.255.255.255/32 '
                                    '-p udp -m udp --sport 68 --dport 67 '
                                    '-j RETURN', comment=ic.DHCP_CLIENT)]
        # Allow neighbor solicitation and multicast listener discovery
        # from the unspecified address for duplicate address detection
        for icmp6_type in constants.ICMPV6_ALLOWED_UNSPEC_ADDR_TYPES:
            ipv6_rules += [comment_rule('-s ::/128 -d ff02::/16 '
                                        '-p ipv6-icmp -m icmp6 '
                                        '--icmpv6-type %s -j RETURN' %
                                        icmp6_type,
                                        comment=ic.IPV6_ICMP_ALLOW)]
        mac_ipv4_pairs = []
        mac_ipv6_pairs = []

        if isinstance(port.get('allowed_address_pairs'), list):
            for address_pair in port['allowed_address_pairs']:
                self._build_ipv4v6_mac_ip_list(address_pair['mac_address'],
                                               address_pair['ip_address'],
                                               mac_ipv4_pairs,
                                               mac_ipv6_pairs)

        for ip in port['fixed_ips']:
            self._build_ipv4v6_mac_ip_list(port['mac_address'], ip,
                                           mac_ipv4_pairs, mac_ipv6_pairs)
        if not port['fixed_ips']:
            mac_ipv4_pairs.append((port['mac_address'], None))
            mac_ipv6_pairs.append((port['mac_address'], None))

        self._setup_spoof_filter_chain(port, self.iptables.ipv4['filter'],
                                       mac_ipv4_pairs, ipv4_rules)
        self._setup_spoof_filter_chain(port, self.iptables.ipv6['filter'],
                                       mac_ipv6_pairs, ipv6_rules)
        # Fixed rules for traffic after source address is verified
        # Allow dhcp client renewal and rebinding
        ipv4_rules += [comment_rule('-p udp -m udp --sport 68 --dport 67 '
                                    '-j RETURN', comment=ic.DHCP_CLIENT)]
        # Drop Router Advts from the port.
        ipv6_rules += [comment_rule('-p ipv6-icmp -m icmp6 --icmpv6-type %s '
                                    '-j DROP' % constants.ICMPV6_TYPE_RA,
                                    comment=ic.IPV6_RA_DROP)]
        ipv6_rules += [comment_rule('-p ipv6-icmp -j RETURN',
                                    comment=ic.IPV6_ICMP_ALLOW)]
        ipv6_rules += [comment_rule('-p udp -m udp --sport 546 '
                                    '-m udp --dport 547 '
                                    '-j RETURN', comment=ic.DHCP_CLIENT)]

    def _drop_dhcp_rule(self, ipv4_rules, ipv6_rules):
        #Note(nati) Drop dhcp packet from VM
        ipv4_rules += [comment_rule('-p udp -m udp --sport 67 '
                                    '-m udp --dport 68 '
                                    '-j DROP', comment=ic.DHCP_SPOOF)]
        ipv6_rules += [comment_rule('-p udp -m udp --sport 547 '
                                    '-m udp --dport 546 '
                                    '-j DROP', comment=ic.DHCP_SPOOF)]

    def _accept_inbound_icmpv6(self):
        # Allow multicast listener, neighbor solicitation and
        # neighbor advertisement into the instance
        icmpv6_rules = []
        for icmp6_type in firewall.ICMPV6_ALLOWED_TYPES:
            icmpv6_rules += ['-p ipv6-icmp -m icmp6 --icmpv6-type %s '
                             '-j RETURN' % icmp6_type]
        return icmpv6_rules

    def _select_sg_rules_for_port(self, port, direction):
        """Select rules from the security groups the port is member of."""
        port_sg_ids = port.get('security_groups', [])
        port_rules = []

        for sg_id in port_sg_ids:
            for rule in self.sg_rules.get(sg_id, []):
                if rule['direction'] == direction:
                    if self.enable_ipset:
                        port_rules.append(rule)
                    else:
                        port_rules.extend(
                            self._expand_sg_rule_with_remote_ips(
                                rule, port, direction))
        return port_rules

    def _expand_sg_rule_with_remote_ips(self, rule, port, direction):
        """Expand a remote group rule to rule per remote group IP."""
        remote_group_id = rule.get('remote_group_id')
        if remote_group_id:
            ethertype = rule['ethertype']
            port_ips = port.get('fixed_ips', [])

            for ip in self.sg_members[remote_group_id][ethertype]:
                if ip not in port_ips:
                    ip_rule = rule.copy()
                    direction_ip_prefix = firewall.DIRECTION_IP_PREFIX[
                        direction]
                    ip_prefix = str(netaddr.IPNetwork(ip).cidr)
                    ip_rule[direction_ip_prefix] = ip_prefix
                    yield ip_rule
        else:
            yield rule

    def _get_remote_sg_ids(self, port, direction=None):
        sg_ids = port.get('security_groups', [])
        remote_sg_ids = {constants.IPv4: set(), constants.IPv6: set()}
        for sg_id in sg_ids:
            for rule in self.sg_rules.get(sg_id, []):
                if not direction or rule['direction'] == direction:
                    remote_sg_id = rule.get('remote_group_id')
                    ether_type = rule.get('ethertype')
                    if remote_sg_id and ether_type:
                        remote_sg_ids[ether_type].add(remote_sg_id)
        return remote_sg_ids

    def _add_rules_by_security_group(self, port, direction):
        # select rules for current port and direction
        security_group_rules = self._select_sgr_by_direction(port, direction)
        security_group_rules += self._select_sg_rules_for_port(port, direction)
        # split groups by ip version
        # for ipv4, iptables command is used
        # for ipv6, iptables6 command is used
        ipv4_sg_rules, ipv6_sg_rules = self._split_sgr_by_ethertype(
            security_group_rules)
        ipv4_iptables_rules = []
        ipv6_iptables_rules = []
        # include fixed egress/ingress rules
        if direction == firewall.EGRESS_DIRECTION:
            self._add_fixed_egress_rules(port,
                                         ipv4_iptables_rules,
                                         ipv6_iptables_rules)
        elif direction == firewall.INGRESS_DIRECTION:
            ipv6_iptables_rules += self._accept_inbound_icmpv6()
        # include IPv4 and IPv6 iptable rules from security group
        ipv4_iptables_rules += self._convert_sgr_to_iptables_rules(
            ipv4_sg_rules)
        ipv6_iptables_rules += self._convert_sgr_to_iptables_rules(
            ipv6_sg_rules)
        # finally add the rules to the port chain for a given direction
        self._add_rules_to_chain_v4v6(self._port_chain_name(port, direction),
                                      ipv4_iptables_rules,
                                      ipv6_iptables_rules)

    def _add_fixed_egress_rules(self, port, ipv4_iptables_rules,
                                ipv6_iptables_rules):
        self._spoofing_rule(port,
                            ipv4_iptables_rules,
                            ipv6_iptables_rules)
        self._drop_dhcp_rule(ipv4_iptables_rules, ipv6_iptables_rules)

    def _generate_ipset_rule_args(self, sg_rule, remote_gid):
        ethertype = sg_rule.get('ethertype')
        ipset_name = self.ipset.get_name(remote_gid, ethertype)
        if not self.ipset.set_name_exists(ipset_name):
            #NOTE(mangelajo): ipsets for empty groups are not created
            #                 thus we can't reference them.
            return None
        ipset_direction = IPSET_DIRECTION[sg_rule.get('direction')]
        args = self._generate_protocol_and_port_args(sg_rule)
        args += ['-m set', '--match-set', ipset_name, ipset_direction]
        args += ['-j RETURN']
        return args

    def _generate_protocol_and_port_args(self, sg_rule):
        args = self._protocol_arg(sg_rule.get('protocol'))
        args += self._port_arg('sport',
                               sg_rule.get('protocol'),
                               sg_rule.get('source_port_range_min'),
                               sg_rule.get('source_port_range_max'))
        args += self._port_arg('dport',
                               sg_rule.get('protocol'),
                               sg_rule.get('port_range_min'),
                               sg_rule.get('port_range_max'))
        return args

    def _generate_plain_rule_args(self, sg_rule):
        # These arguments MUST be in the format iptables-save will
        # display them: source/dest, protocol, sport, dport, target
        # Otherwise the iptables_manager code won't be able to find
        # them to preserve their [packet:byte] counts.
        args = self._ip_prefix_arg('s', sg_rule.get('source_ip_prefix'))
        args += self._ip_prefix_arg('d', sg_rule.get('dest_ip_prefix'))
        args += self._generate_protocol_and_port_args(sg_rule)
        args += ['-j RETURN']
        return args

    def _convert_sg_rule_to_iptables_args(self, sg_rule):
        remote_gid = sg_rule.get('remote_group_id')
        if self.enable_ipset and remote_gid:
            return self._generate_ipset_rule_args(sg_rule, remote_gid)
        else:
            return self._generate_plain_rule_args(sg_rule)

    def _convert_sgr_to_iptables_rules(self, security_group_rules):
        iptables_rules = []
        self._allow_established(iptables_rules)
        seen_sg_rules = set()
        for rule in security_group_rules:
            args = self._convert_sg_rule_to_iptables_args(rule)
            if args:
                rule_command = ' '.join(args)
                if rule_command in seen_sg_rules:
                    # since these rules are from multiple security groups,
                    # there may be duplicates so we prune them out here
                    continue
                seen_sg_rules.add(rule_command)
                iptables_rules.append(rule_command)

        self._drop_invalid_packets(iptables_rules)
        iptables_rules += [comment_rule('-j $sg-fallback',
                                        comment=ic.UNMATCHED)]
        return iptables_rules

    def _drop_invalid_packets(self, iptables_rules):
        # Always drop invalid packets
        iptables_rules += [comment_rule('-m state --state ' 'INVALID -j DROP',
                                        comment=ic.INVALID_DROP)]
        return iptables_rules

    def _allow_established(self, iptables_rules):
        # Allow established connections
        iptables_rules += [comment_rule(
            '-m state --state RELATED,ESTABLISHED -j RETURN',
            comment=ic.ALLOW_ASSOC)]
        return iptables_rules

    def _protocol_arg(self, protocol):
        if not protocol:
            return []
        if protocol == 'icmpv6':
            protocol = 'ipv6-icmp'
        iptables_rule = ['-p', protocol]
        return iptables_rule

    def _port_arg(self, direction, protocol, port_range_min, port_range_max):
        if (protocol not in ['udp', 'tcp', 'icmp', 'ipv6-icmp']
            or port_range_min is None):
            return []

        protocol_modules = {'udp': 'udp', 'tcp': 'tcp',
                            'icmp': 'icmp', 'ipv6-icmp': 'icmp6'}
        # iptables adds '-m protocol' when the port number is specified
        args = ['-m', protocol_modules[protocol]]

        if protocol in ['icmp', 'ipv6-icmp']:
            protocol_type = 'icmpv6' if protocol == 'ipv6-icmp' else 'icmp'
            # Note(xuhanp): port_range_min/port_range_max represent
            # icmp type/code when protocol is icmp or icmpv6
            args += ['--%s-type' % protocol_type, '%s' % port_range_min]
            # icmp code can be 0 so we cannot use "if port_range_max" here
            if port_range_max is not None:
                args[-1] += '/%s' % port_range_max
        elif port_range_min == port_range_max:
            args += ['--%s' % direction, '%s' % (port_range_min,)]
        else:
            args += ['-m', 'multiport', '--%ss' % direction,
                     '%s:%s' % (port_range_min, port_range_max)]
        return args

    def _ip_prefix_arg(self, direction, ip_prefix):
        #NOTE (nati) : source_group_id is converted to list of source_
        # ip_prefix in server side
        if ip_prefix:
            if '/' not in ip_prefix:
                # we need to convert it into a prefix to match iptables
                ip_prefix = c_utils.ip_to_cidr(ip_prefix)
            elif ip_prefix.endswith('/0'):
                # an allow for every address is not a constraint so
                # iptables drops it
                return []
            return ['-%s' % direction, ip_prefix]
        return []

    def _port_chain_name(self, port, direction):
        return iptables_manager.get_chain_name(
            '%s%s' % (CHAIN_NAME_PREFIX[direction], port['device'][3:]))

    def filter_defer_apply_on(self):
        if not self._defer_apply:
            self.iptables.defer_apply_on()
            self._pre_defer_filtered_ports = dict(self.filtered_ports)
            self._pre_defer_unfiltered_ports = dict(self.unfiltered_ports)
            self.pre_sg_members = dict(self.sg_members)
            self.pre_sg_rules = dict(self.sg_rules)
            self._defer_apply = True

    def _remove_unused_security_group_info(self):
        """Remove any unnecessary local security group info or unused ipsets.

        This function has to be called after applying the last iptables
        rules, so we're in a point where no iptable rule depends
        on an ipset we're going to delete.
        """
        filtered_ports = self.filtered_ports.values()

        remote_sgs_to_remove = self._determine_remote_sgs_to_remove(
            filtered_ports)

        for ip_version, remote_sg_ids in six.iteritems(remote_sgs_to_remove):
            if self.enable_ipset:
                self._remove_ipsets_for_remote_sgs(ip_version, remote_sg_ids)

        self._remove_sg_members(remote_sgs_to_remove)

        # Remove unused security group rules
        for remove_group_id in self._determine_sg_rules_to_remove(
                filtered_ports):
            self.sg_rules.pop(remove_group_id, None)

    def _determine_remote_sgs_to_remove(self, filtered_ports):
        """Calculate which remote security groups we don't need anymore.

        We do the calculation for each ip_version.
        """
        sgs_to_remove_per_ipversion = {constants.IPv4: set(),
                                       constants.IPv6: set()}
        remote_group_id_sets = self._get_remote_sg_ids_sets_by_ipversion(
            filtered_ports)
        for ip_version, remote_group_id_set in (
                six.iteritems(remote_group_id_sets)):
            sgs_to_remove_per_ipversion[ip_version].update(
                set(self.pre_sg_members) - remote_group_id_set)
        return sgs_to_remove_per_ipversion

    def _get_remote_sg_ids_sets_by_ipversion(self, filtered_ports):
        """Given a port, calculates the remote sg references by ip_version."""
        remote_group_id_sets = {constants.IPv4: set(),
                                constants.IPv6: set()}
        for port in filtered_ports:
            remote_sg_ids = self._get_remote_sg_ids(port)
            for ip_version in (constants.IPv4, constants.IPv6):
                remote_group_id_sets[ip_version] |= remote_sg_ids[ip_version]
        return remote_group_id_sets

    def _determine_sg_rules_to_remove(self, filtered_ports):
        """Calculate which security groups need to be removed.

        We find out by subtracting our previous sg group ids,
        with the security groups associated to a set of ports.
        """
        port_group_ids = self._get_sg_ids_set_for_ports(filtered_ports)
        return set(self.pre_sg_rules) - port_group_ids

    def _get_sg_ids_set_for_ports(self, filtered_ports):
        """Get the port security group ids as a set."""
        port_group_ids = set()
        for port in filtered_ports:
            port_group_ids.update(port.get('security_groups', []))
        return port_group_ids

    def _remove_ipsets_for_remote_sgs(self, ip_version, remote_sg_ids):
        """Remove system ipsets matching the provided parameters."""
        for remote_sg_id in remote_sg_ids:
            self.ipset.destroy(remote_sg_id, ip_version)

    def _remove_sg_members(self, remote_sgs_to_remove):
        """Remove sg_member entries."""
        ipv4_sec_group_set = remote_sgs_to_remove.get(constants.IPv4)
        ipv6_sec_group_set = remote_sgs_to_remove.get(constants.IPv6)
        for sg_id in (ipv4_sec_group_set & ipv6_sec_group_set):
            if sg_id in self.sg_members:
                del self.sg_members[sg_id]

    def _find_deleted_sg_rules(self, sg_id):
        del_rules = list()
        for pre_rule in self.pre_sg_rules.get(sg_id, []):
            if pre_rule not in self.sg_rules.get(sg_id, []):
                del_rules.append(pre_rule)
        return del_rules

    def _find_devices_on_security_group(self, sg_id):
        device_list = list()
        for device in self.filtered_ports.values():
            if sg_id in device.get('security_groups', []):
                device_list.append(device)
        return device_list

    def _clean_deleted_sg_rule_conntrack_entries(self):
        deleted_sg_ids = set()
        for sg_id in self.updated_rule_sg_ids:
            del_rules = self._find_deleted_sg_rules(sg_id)
            if not del_rules:
                continue
            device_list = self._find_devices_on_security_group(sg_id)
            for rule in del_rules:
                self.ipconntrack.delete_conntrack_state_by_rule(
                    device_list, rule)
            deleted_sg_ids.add(sg_id)
        for id in deleted_sg_ids:
            self.updated_rule_sg_ids.remove(id)

    def _clean_updated_sg_member_conntrack_entries(self):
        updated_device_ids = set()
        for device in self.updated_sg_members:
            sec_group_change = False
            device_info = self.filtered_ports.get(device)
            pre_device_info = self._pre_defer_filtered_ports.get(device)
            if not device_info or not pre_device_info:
                continue
            for sg_id in pre_device_info.get('security_groups', []):
                if sg_id not in device_info.get('security_groups', []):
                    sec_group_change = True
                    break
            if not sec_group_change:
                continue
            for ethertype in [constants.IPv4, constants.IPv6]:
                self.ipconntrack.delete_conntrack_state_by_remote_ips(
                    [device_info], ethertype, set())
            updated_device_ids.add(device)
        for id in updated_device_ids:
            self.updated_sg_members.remove(id)

    def _clean_deleted_remote_sg_members_conntrack_entries(self):
        deleted_sg_ids = set()
        for sg_id, devices in self.devices_with_updated_sg_members.items():
            for ethertype in [constants.IPv4, constants.IPv6]:
                pre_ips = self._get_sg_members(
                    self.pre_sg_members, sg_id, ethertype)
                cur_ips = self._get_sg_members(
                    self.sg_members, sg_id, ethertype)
                ips = (pre_ips - cur_ips)
                if devices and ips:
                    self.ipconntrack.delete_conntrack_state_by_remote_ips(
                        devices, ethertype, ips)
            deleted_sg_ids.add(sg_id)
        for id in deleted_sg_ids:
            self.devices_with_updated_sg_members.pop(id, None)

    def _remove_conntrack_entries_from_sg_updates(self):
        self._clean_deleted_sg_rule_conntrack_entries()
        self._clean_updated_sg_member_conntrack_entries()
        if not self.enable_ipset:
            self._clean_deleted_remote_sg_members_conntrack_entries()

    def _get_sg_members(self, sg_info, sg_id, ethertype):
        return set(sg_info.get(sg_id, {}).get(ethertype, []))

    def filter_defer_apply_off(self):
        if self._defer_apply:
            self._defer_apply = False
            self._remove_chains_apply(self._pre_defer_filtered_ports,
                                      self._pre_defer_unfiltered_ports)
            self._setup_chains_apply(self.filtered_ports,
                                     self.unfiltered_ports)
            self.iptables.defer_apply_off()
            self._remove_conntrack_entries_from_sg_updates()
            self._remove_unused_security_group_info()
            self._pre_defer_filtered_ports = None
            self._pre_defer_unfiltered_ports = None


class OVSHybridIptablesFirewallDriver(IptablesFirewallDriver):
    OVS_HYBRID_TAP_PREFIX = constants.TAP_DEVICE_PREFIX
    OVS_HYBRID_PLUG_REQUIRED = True
    CONNTRACK_ZONE_PER_PORT = True

    def _port_chain_name(self, port, direction):
        return iptables_manager.get_chain_name(
            '%s%s' % (CHAIN_NAME_PREFIX[direction], port['device']))

    def _get_br_device_name(self, port):
        return ('qvb' + port['device'])[:n_const.LINUX_DEV_LEN]

    def _get_device_name(self, port):
        return get_hybrid_port_name(port['device'])