Ejemplo n.º 1
0
 def _remove_chains(self):
     try:
         self.iptables_updater.delete_chains(chain_names(self._suffix),
                                             async=True)
     except FailedSystemCall:
         _log.exception("Failed to delete chains for %s", self)
         self._failed = True
Ejemplo n.º 2
0
 def _remove_chains(self):
     try:
         self.iptables_updater.delete_chains(chain_names(self._suffix),
                                             async=True)
     except CalledProcessError:
         _log.exception("Failed to delete chains for %s", self)
         self._failed = True
Ejemplo n.º 3
0
 def _remove_chains(self):
     try:
         self.iptables_updater.delete_chains(chain_names(self._suffix),
                                             async=True)
     except FailedSystemCall:
         _log.exception("Failed to delete chains for %s", self)
     else:
         self._iptables_in_sync = True
Ejemplo n.º 4
0
 def _remove_chains(self):
     try:
         self.iptables_updater.delete_chains(chain_names(self._suffix),
                                             async=True)
     except FailedSystemCall:
         _log.exception("Failed to delete chains for %s", self)
     else:
         self._iptables_in_sync = True
         self._chains_programmed = False
Ejemplo n.º 5
0
def _get_endpoint_rules(endpoint_id, suffix, mac, profile_ids):
    to_chain_name, from_chain_name = chain_names(suffix)

    to_chain, to_deps = _build_to_or_from_chain(endpoint_id, profile_ids, to_chain_name, "inbound")
    from_chain, from_deps = _build_to_or_from_chain(
        endpoint_id, profile_ids, from_chain_name, "outbound", expected_mac=mac
    )

    updates = {to_chain_name: to_chain, from_chain_name: from_chain}
    deps = {to_chain_name: to_deps, from_chain_name: from_deps}
    return updates, deps
Ejemplo n.º 6
0
def _get_endpoint_rules(endpoint_id, suffix, mac, profile_ids):
    to_chain_name, from_chain_name = chain_names(suffix)

    to_chain, to_deps = _build_to_or_from_chain(endpoint_id, profile_ids,
                                                to_chain_name, "inbound")
    from_chain, from_deps = _build_to_or_from_chain(
        endpoint_id,
        profile_ids,
        from_chain_name,
        "outbound",
        expected_mac=mac,
    )

    updates = {to_chain_name: to_chain, from_chain_name: from_chain}
    deps = {to_chain_name: to_deps, from_chain_name: from_deps}
    return updates, deps
Ejemplo n.º 7
0
 def _update_chains(self):
     updates, deps = _get_endpoint_rules(self.combined_id.endpoint,
                                         self._suffix,
                                         self._mac,
                                         self.endpoint["profile_ids"])
     try:
         self.iptables_updater.rewrite_chains(updates, deps, async=False)
     except FailedSystemCall:
         _log.exception("Failed to program chains for %s. Removing.", self)
         try:
             self.iptables_updater.delete_chains(chain_names(self._suffix),
                                                 async=False)
         except FailedSystemCall:
             _log.exception("Failed to remove chains after original "
                            "failure")
     else:
         self._iptables_in_sync = True
Ejemplo n.º 8
0
 def _update_chains(self):
     updates, deps = _get_endpoint_rules(self.combined_id.endpoint,
                                         self._suffix, self._mac,
                                         self.endpoint["profile_ids"])
     try:
         self.iptables_updater.rewrite_chains(updates, deps, async=False)
     except FailedSystemCall:
         _log.exception("Failed to program chains for %s. Removing.", self)
         try:
             self.iptables_updater.delete_chains(chain_names(self._suffix),
                                                 async=False)
         except FailedSystemCall:
             _log.exception("Failed to remove chains after original "
                            "failure")
     else:
         self._iptables_in_sync = True
         self._chains_programmed = True
Ejemplo n.º 9
0
    def _calculate_update(self, ifaces):
        """
        Calculates the iptables update to rewrite our chains.

        To avoid traversing lots of dispatch rules to find the right one,
        we build a tree of chains.  Currently, the tree can only be
        two layers deep: a root chain and a layer of leaves.

        Interface names look like this: "prefix1234abc".  The "prefix"
        part is always the same so we ignore it.  We call "1234abc", the
        "suffix".

        The root chain contains two sorts of rules:

        * where there are multiple interfaces whose suffixes start with
          the same character, it contains a rule that matches on
          that prefix of the "suffix"(!) and directs the packet to a
          leaf chain for that prefix.

        * as an optimization, if there is only one interface whose
          suffix starts with a given character, it contains a dispatch
          rule for that exact interface name.

        For example, if we have interface names "tapA1" "tapB1" "tapB2",
        we'll get (in pseudo code):

        Root chain:

        if interface=="tapA1" then goto chain for endpoint tapA1
        if interface.startswith("tapB") then goto leaf chain for prefix "tapB"

        tapB leaf chain:

        if interface=="tapB1" then goto chain for endpoint tapB1
        if interface=="tapB2" then goto chain for endpoint tapB2

        :param set[str] ifaces: The list of interfaces to generate a
            dispatch chain for.
        :returns Tuple: to_delete, deps, updates, new_leaf_chains:

            * set of leaf chains that are no longer needed for deletion
            * chain dependency dict.
            * chain updates dict.
            * complete set of leaf chains that are now required.
        """

        # iptables update fragments/dependencies for the root chains.
        updates = defaultdict(list)
        root_to_upds = updates[CHAIN_TO_ENDPOINT]
        root_from_upds = updates[CHAIN_FROM_ENDPOINT]

        dependencies = defaultdict(set)
        root_to_deps = dependencies[CHAIN_TO_ENDPOINT]
        root_from_deps = dependencies[CHAIN_FROM_ENDPOINT]

        # Special case: allow the metadata IP through from all interfaces.
        if self.config.METADATA_IP is not None and self.ip_version == 4:
            # Need to allow outgoing Metadata requests.
            root_from_upds.append("--append %s "
                                  "--protocol tcp "
                                  "--in-interface %s+ "
                                  "--destination %s "
                                  "--dport %s "
                                  "--jump RETURN" %
                                  (CHAIN_FROM_ENDPOINT,
                                   self.config.IFACE_PREFIX,
                                   self.config.METADATA_IP,
                                   self.config.METADATA_PORT))

        # Separate the interface names by their prefixes so we can count them
        # and decide whether to program a leaf chain or not.
        interfaces_by_prefix = defaultdict(set)
        for iface in ifaces:
            ep_suffix = interface_to_suffix(self.config, iface)
            prefix = ep_suffix[:1]
            interfaces_by_prefix[prefix].add(iface)

        # Spin through the interfaces by prefix.  Either add them directly
        # to the root chain or create a leaf and add them there.
        new_leaf_chains = set()
        for prefix, interfaces in interfaces_by_prefix.iteritems():
            use_root_chain = len(interfaces) == 1
            if use_root_chain:
                # Optimization: there's only one interface with this prefix,
                # don't program a leaf chain.
                disp_to_chain = CHAIN_TO_ENDPOINT
                disp_from_chain = CHAIN_FROM_ENDPOINT
                to_deps = root_to_deps
                from_deps = root_from_deps
                to_upds = root_to_upds
                from_upds = root_from_upds
            else:
                # There's more than one interface with this prefix, program
                # a leaf chain.
                disp_to_chain = CHAIN_TO_LEAF + "-" + prefix
                disp_from_chain = CHAIN_FROM_LEAF + "-" + prefix
                to_upds = updates[disp_to_chain]
                from_upds = updates[disp_from_chain]
                to_deps = dependencies[disp_to_chain]
                from_deps = dependencies[disp_from_chain]
                new_leaf_chains.add(disp_from_chain)
                new_leaf_chains.add(disp_to_chain)
                # Root chain depends on its leaves.
                root_from_deps.add(disp_to_chain)
                root_to_deps.add(disp_from_chain)
                # Point root chain at prefix chain.
                iface_match = self.config.IFACE_PREFIX + prefix + "+"
                root_from_upds.append(
                    "--append %s --in-interface %s --goto %s" %
                    (CHAIN_FROM_ENDPOINT, iface_match, disp_from_chain)
                )
                root_to_upds.append(
                    "--append %s --out-interface %s --goto %s" %
                    (CHAIN_TO_ENDPOINT, iface_match, disp_to_chain)
                )

            # Common processing, add the per-endpoint rules to whichever
            # chain we decided above.
            for iface in interfaces:
                # Add rule to leaf or global chain to direct traffic to the
                # endpoint-specific one.  Note that we use --goto, which means
                # that the endpoint-specific chain will return to our parent
                # rather than to this chain.
                ep_suffix = interface_to_suffix(self.config, iface)
                to_chain_name, from_chain_name = chain_names(ep_suffix)
                from_upds.append("--append %s --in-interface %s --goto %s" %
                                 (disp_from_chain, iface, from_chain_name))
                from_deps.add(from_chain_name)
                to_upds.append("--append %s --out-interface %s --goto %s" %
                               (disp_to_chain, iface, to_chain_name))
                to_deps.add(to_chain_name)

            if not use_root_chain:
                # Add a default drop to the end of the leaf chain.
                from_upds.append("--append %s --jump DROP" % disp_from_chain)
                to_upds.append("--append %s --jump DROP" % disp_to_chain)

        # Both TO and FROM chains end with a DROP so that interfaces that
        # we don't know about yet can't bypass our rules.
        root_from_upds.append("--append %s --jump DROP" % CHAIN_FROM_ENDPOINT)
        root_to_upds.append("--append %s --jump DROP" % CHAIN_TO_ENDPOINT)
        chains_to_delete = self.programmed_leaf_chains - new_leaf_chains

        return chains_to_delete, dependencies, updates, new_leaf_chains
Ejemplo n.º 10
0
def _get_endpoint_rules(endpoint_id, suffix, ip_version, local_ips, mac,
                        profile_ids):
    to_chain_name, from_chain_name = chain_names(suffix)

    # First build the chain that manages packets to the interface.
    # Common chain prefixes.  Allow IPv6 ICMP and conntrack rules.
    to_chain = ["--flush %s" % to_chain_name]
    if ip_version == 6:
        #  In ipv6 only, there are 6 rules that need to be created first.
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmptype 130
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmptype 131
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmptype 132
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmp router-advertisement
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmp neighbour-solicitation
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmp neighbour-advertisement
        #
        #  These rules are ICMP types 130, 131, 132, 134, 135 and 136.
        for icmp_type in ["130", "131", "132", "134", "135", "136"]:
            to_chain.append("--append %s --jump RETURN "
                            "--protocol ipv6-icmp "
                            "--icmpv6-type %s" % (to_chain_name, icmp_type))
    to_chain.append("--append %s --match conntrack --ctstate INVALID "
                    "--jump DROP" % to_chain_name)
    to_chain.append("--append %s --match conntrack "
                    "--ctstate RELATED,ESTABLISHED --jump RETURN" %
                    to_chain_name)

    # Jump to each profile in turn.  The profile will do one of the
    # following:
    # * DROP the packet; in which case we won't see it again.
    # * RETURN without MARKing the packet; indicates that the packet was
    #   accepted.
    # * RETURN with a mark on the packet, indicates reaching the end
    #   of the chain.
    to_deps = set()
    for profile_id in profile_ids:
        profile_in_chain = profile_to_chain_name("inbound", profile_id)
        to_deps.add(profile_in_chain)
        to_chain.append("--append %s --jump MARK --set-mark 0" %
                        to_chain_name)
        to_chain.append("--append %s --jump %s" %
                        (to_chain_name, profile_in_chain))
        to_chain.append('--append %s --match mark ! --mark 1/1 '
                        '--match comment --comment "No mark means profile '
                        'accepted packet" --jump RETURN' % to_chain_name)

    # Default drop rule.
    to_chain.append(commented_drop_fragment(to_chain_name,
                                            "Endpoint %s:" % endpoint_id))

    # Now the chain that manages packets from the interface...
    from_chain = ["--flush %s" % from_chain_name]
    if ip_version == 6:
        # In ipv6 only, allow all ICMP traffic from this endpoint to anywhere.
        from_chain.append("--append %s --protocol ipv6-icmp --jump RETURN" %
                          from_chain_name)

    # Conntrack rules.
    from_chain.append("--append %s --match conntrack --ctstate INVALID "
                      "--jump DROP" % from_chain_name)
    from_chain.append("--append %s --match conntrack "
                      "--ctstate RELATED,ESTABLISHED --jump RETURN" %
                      from_chain_name)

    if ip_version == 4:
        from_chain.append("--append %s --protocol udp --sport 68 --dport 67 "
                          "--jump RETURN" % from_chain_name)
    else:
        assert ip_version == 6
        from_chain.append("--append %s --protocol udp --sport 546 --dport 547 "
                          "--jump RETURN" % from_chain_name)

    # Combined anti-spoofing and jump to profile rules.  The only way to
    # get to a profile chain is to have the correct IP and MAC address.
    from_deps = set()
    for profile_id in profile_ids:
        profile_out_chain = profile_to_chain_name("outbound", profile_id)
        from_deps.add(profile_out_chain)
        from_chain.append("--append %s --jump MARK --set-mark 0" %
                          from_chain_name)
        for ip in local_ips:
            if "/" in ip:
                cidr = ip
            else:
                cidr = "%s/32" % ip if ip_version == 4 else "%s/128" % ip
            # Jump to each profile in turn.  The profile will do one of the
            # following:
            # * DROP the packet; in which case we won't see it again.
            # * RETURN without MARKing the packet; indicates that the packet
            #   was accepted.
            # * RETURN with a mark on the packet, indicates reaching the end
            #   of the chain.
            from_chain.append("--append %s --src %s --match mac "
                              "--mac-source %s --jump %s" %
                              (from_chain_name, cidr, mac.upper(),
                               profile_out_chain))
        from_chain.append('--append %s --match mark ! --mark 1/1 '
                          '--match comment --comment "No mark means profile '
                          'accepted packet" --jump RETURN' %
                          from_chain_name)

    # Final default DROP if no profile RETURNed or no MAC matched.
    drop_frag = commented_drop_fragment(from_chain_name,
                                        "Default DROP if no match (endpoint %s):" %
                                        endpoint_id)
    from_chain.append(drop_frag)

    updates = {to_chain_name: to_chain, from_chain_name: from_chain}
    deps = {to_chain_name: to_deps, from_chain_name: from_deps}
    return updates, deps
Ejemplo n.º 11
0
def _get_endpoint_rules(endpoint_id, suffix, ip_version, local_ips, mac,
                        profile_ids):
    to_chain_name, from_chain_name = chain_names(suffix)

    # First build the chain that manages packets to the interface.
    # Common chain prefixes.
    to_chain = []

    # Jump to each profile in turn.  The profile will do one of the
    # following:
    # * DROP the packet; in which case we won't see it again.
    # * RETURN without MARKing the packet; indicates that the packet was
    #   accepted.
    # * RETURN with a mark on the packet, indicates reaching the end
    #   of the chain.
    to_deps = set()
    for profile_id in profile_ids:
        profile_in_chain = profile_to_chain_name("inbound", profile_id)
        to_deps.add(profile_in_chain)
        to_chain.append("--append %s --jump MARK --set-mark 0" %
                        to_chain_name)
        to_chain.append("--append %s --jump %s" %
                        (to_chain_name, profile_in_chain))
        to_chain.append('--append %s --match mark ! --mark 1/1 '
                        '--match comment --comment "No mark means profile '
                        'accepted packet" --jump RETURN' % to_chain_name)

    # Default drop rule.
    to_chain.append(commented_drop_fragment(to_chain_name,
                                            "Endpoint %s:" % endpoint_id))

    # Now the chain that manages packets from the interface...
    from_chain = []

    # Combined anti-spoofing and jump to profile rules.  The only way to
    # get to a profile chain is to have the correct IP and MAC address.
    from_deps = set()
    for profile_id in profile_ids:
        profile_out_chain = profile_to_chain_name("outbound", profile_id)
        from_deps.add(profile_out_chain)
        from_chain.append("--append %s --jump MARK --set-mark 0" %
                          from_chain_name)
        for ip in local_ips:
            if "/" in ip:
                cidr = ip
            else:
                cidr = "%s/32" % ip if ip_version == 4 else "%s/128" % ip
            # Jump to each profile in turn.  The profile will do one of the
            # following:
            # * DROP the packet; in which case we won't see it again.
            # * RETURN without MARKing the packet; indicates that the packet
            #   was accepted.
            # * RETURN with a mark on the packet, indicates reaching the end
            #   of the chain.
            from_chain.append("--append %s --src %s --match mac "
                              "--mac-source %s --jump %s" %
                              (from_chain_name, cidr, mac.upper(),
                               profile_out_chain))
        from_chain.append('--append %s --match mark ! --mark 1/1 '
                          '--match comment --comment "No mark means profile '
                          'accepted packet" --jump RETURN' %
                          from_chain_name)

    # Final default DROP if no profile RETURNed or no MAC matched.
    drop_frag = commented_drop_fragment(from_chain_name,
                                        "Default DROP if no match (endpoint %s):" %
                                        endpoint_id)
    from_chain.append(drop_frag)

    updates = {to_chain_name: to_chain, from_chain_name: from_chain}
    deps = {to_chain_name: to_deps, from_chain_name: from_deps}
    return updates, deps
Ejemplo n.º 12
0
def _get_endpoint_rules(endpoint_id, suffix, ip_version, local_ips, mac,
                        profile_ids):
    to_chain_name, from_chain_name = chain_names(suffix)

    # First build the chain that manages packets to the interface.
    # Common chain prefixes.
    to_chain = []

    # Jump to each profile in turn.  The profile will do one of the
    # following:
    # * DROP the packet; in which case we won't see it again.
    # * RETURN without MARKing the packet; indicates that the packet was
    #   accepted.
    # * RETURN with a mark on the packet, indicates reaching the end
    #   of the chain.
    to_deps = set()
    for profile_id in profile_ids:
        profile_in_chain = profile_to_chain_name("inbound", profile_id)
        to_deps.add(profile_in_chain)
        to_chain.append("--append %s --jump MARK --set-mark 0" % to_chain_name)
        to_chain.append("--append %s --jump %s" %
                        (to_chain_name, profile_in_chain))
        to_chain.append('--append %s --match mark ! --mark 1/1 '
                        '--match comment --comment "No mark means profile '
                        'accepted packet" --jump RETURN' % to_chain_name)

    # Default drop rule.
    to_chain.append(
        commented_drop_fragment(to_chain_name, "Endpoint %s:" % endpoint_id))

    # Now the chain that manages packets from the interface...
    from_chain = []

    # Combined anti-spoofing and jump to profile rules.  The only way to
    # get to a profile chain is to have the correct IP and MAC address.
    from_deps = set()
    for profile_id in profile_ids:
        profile_out_chain = profile_to_chain_name("outbound", profile_id)
        from_deps.add(profile_out_chain)
        from_chain.append("--append %s --jump MARK --set-mark 0" %
                          from_chain_name)
        for ip in local_ips:
            if "/" in ip:
                cidr = ip
            else:
                cidr = "%s/32" % ip if ip_version == 4 else "%s/128" % ip
            # Jump to each profile in turn.  The profile will do one of the
            # following:
            # * DROP the packet; in which case we won't see it again.
            # * RETURN without MARKing the packet; indicates that the packet
            #   was accepted.
            # * RETURN with a mark on the packet, indicates reaching the end
            #   of the chain.
            from_chain.append(
                "--append %s --src %s --match mac "
                "--mac-source %s --jump %s" %
                (from_chain_name, cidr, mac.upper(), profile_out_chain))
        from_chain.append('--append %s --match mark ! --mark 1/1 '
                          '--match comment --comment "No mark means profile '
                          'accepted packet" --jump RETURN' % from_chain_name)

    # Final default DROP if no profile RETURNed or no MAC matched.
    drop_frag = commented_drop_fragment(
        from_chain_name,
        "Default DROP if no match (endpoint %s):" % endpoint_id)
    from_chain.append(drop_frag)

    updates = {to_chain_name: to_chain, from_chain_name: from_chain}
    deps = {to_chain_name: to_deps, from_chain_name: from_deps}
    return updates, deps
Ejemplo n.º 13
0
def _get_endpoint_rules(endpoint_id, suffix, ip_version, local_ips, mac,
                        profile_id):
    to_chain_name, from_chain_name = chain_names(suffix)

    to_chain = ["--flush %s" % to_chain_name]
    if ip_version == 6:
        #  In ipv6 only, there are 6 rules that need to be created first.
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmptype 130
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmptype 131
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmptype 132
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmp router-advertisement
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmp neighbour-solicitation
        #  RETURN ipv6-icmp anywhere anywhere ipv6-icmp neighbour-advertisement
        #
        #  These rules are ICMP types 130, 131, 132, 134, 135 and 136, and can
        #  be created on the command line with something like :
        #     ip6tables -A plw -j RETURN --protocol ipv6-icmp --icmpv6-type 130
        for icmp_type in ["130", "131", "132", "134", "135", "136"]:
            to_chain.append("--append %s --jump RETURN "
                            "--protocol ipv6-icmp "
                            "--icmpv6-type %s" % (to_chain_name, icmp_type))
    to_chain.append("--append %s --match conntrack --ctstate INVALID "
                    "--jump DROP" % to_chain_name)
    to_chain.append("--append %s --match conntrack "
                    "--ctstate RELATED,ESTABLISHED --jump RETURN" %
                    to_chain_name)
    assert profile_id, "Profile ID should be set, not %s" % profile_id
    profile_in_chain = profile_to_chain_name("inbound", profile_id)
    to_chain.append("--append %s --goto %s" %
                    (to_chain_name, profile_in_chain))
    # This drop rule is not hittable, but it gives us a place to stash the
    # comment with our ID.
    to_chain.append(commented_drop_fragment(to_chain_name,
                                            "Endpoint %s:" % endpoint_id))
    to_deps = set([profile_in_chain])

    # Now the chain that manages packets from the interface...
    from_chain = ["--flush %s" % from_chain_name]
    if ip_version == 6:
        # In ipv6 only, allows all ICMP traffic from this endpoint to anywhere.
        from_chain.append("--append %s --protocol ipv6-icmp --jump RETURN" %
                          from_chain_name)

    # Conntrack rules.
    from_chain.append("--append %s --match conntrack --ctstate INVALID "
                      "--jump DROP" % from_chain_name)
    from_chain.append("--append %s --match conntrack "
                      "--ctstate RELATED,ESTABLISHED --jump RETURN" %
                      from_chain_name)

    if ip_version == 4:
        from_chain.append("--append %s --protocol udp --sport 68 --dport 67 "
                          "--jump RETURN" % from_chain_name)
    else:
        assert ip_version == 6
        from_chain.append("--append %s --protocol udp --sport 546 --dport 547 "
                          "--jump RETURN" % from_chain_name)

    # Anti-spoofing rules.  Only allow traffic from known (IP, MAC) pairs to
    # get to the profile chain, drop other traffic.
    profile_out_chain = profile_to_chain_name("outbound", profile_id)
    from_deps = set([profile_out_chain])
    for ip in local_ips:
        if "/" in ip:
            cidr = ip
        else:
            cidr = "%s/32" % ip if ip_version == 4 else "%s/128" % ip
        # Note use of --goto rather than --jump; this means that when the
        # profile chain returns, it will return the chain that called us, not
        # this chain.
        from_chain.append("--append %s --src %s --match mac --mac-source %s "
                          "--goto %s" % (from_chain_name, cidr,
                                         mac.upper(), profile_out_chain))
    drop_frag = commented_drop_fragment(from_chain_name,
                                        "Anti-spoof DROP (endpoint %s):" %
                                        endpoint_id)
    from_chain.append(drop_frag)

    updates = {to_chain_name: to_chain, from_chain_name: from_chain}
    deps = {to_chain_name: to_deps, from_chain_name: from_deps}
    return updates, deps
Ejemplo n.º 14
0
    def _calculate_update(self, ifaces):
        """
        Calculates the iptables update to rewrite our chains.

        To avoid traversing lots of dispatch rules to find the right one,
        we build a tree of chains.  Currently, the tree can only be
        two layers deep: a root chain and a layer of leaves.

        Interface names look like this: "prefix1234abc".  The "prefix"
        part is always the same so we ignore it.  We call "1234abc", the
        "suffix".

        The root chain contains two sorts of rules:

        * where there are multiple interfaces whose suffixes start with
          the same character, it contains a rule that matches on
          that prefix of the "suffix"(!) and directs the packet to a
          leaf chain for that prefix.

        * as an optimization, if there is only one interface whose
          suffix starts with a given character, it contains a dispatch
          rule for that exact interface name.

        For example, if we have interface names "tapA1" "tapB1" "tapB2",
        we'll get (in pseudo code):

        Root chain:

        if interface=="tapA1" then goto chain for endpoint tapA1
        if interface.startswith("tapB") then goto leaf chain for prefix "tapB"

        tapB leaf chain:

        if interface=="tapB1" then goto chain for endpoint tapB1
        if interface=="tapB2" then goto chain for endpoint tapB2

        :param set[str] ifaces: The list of interfaces to generate a
            dispatch chain for.
        :returns Tuple: to_delete, deps, updates, new_leaf_chains:

            * set of leaf chains that are no longer needed for deletion
            * chain dependency dict.
            * chain updates dict.
            * complete set of leaf chains that are now required.
        """

        # iptables update fragments/dependencies for the root chains.
        updates = defaultdict(list)
        root_to_upds = updates[CHAIN_TO_ENDPOINT]
        root_from_upds = updates[CHAIN_FROM_ENDPOINT]

        dependencies = defaultdict(set)
        root_to_deps = dependencies[CHAIN_TO_ENDPOINT]
        root_from_deps = dependencies[CHAIN_FROM_ENDPOINT]

        # Separate the interface names by their prefixes so we can count them
        # and decide whether to program a leaf chain or not.
        interfaces_by_prefix = defaultdict(set)
        for iface in ifaces:
            ep_suffix = interface_to_suffix(self.config, iface)
            prefix = ep_suffix[:1]
            interfaces_by_prefix[prefix].add(iface)

        # Spin through the interfaces by prefix.  Either add them directly
        # to the root chain or create a leaf and add them there.
        new_leaf_chains = set()
        for prefix, interfaces in interfaces_by_prefix.iteritems():
            use_root_chain = len(interfaces) == 1
            if use_root_chain:
                # Optimization: there's only one interface with this prefix,
                # don't program a leaf chain.
                disp_to_chain = CHAIN_TO_ENDPOINT
                disp_from_chain = CHAIN_FROM_ENDPOINT
                to_deps = root_to_deps
                from_deps = root_from_deps
                to_upds = root_to_upds
                from_upds = root_from_upds
            else:
                # There's more than one interface with this prefix, program
                # a leaf chain.
                disp_to_chain = CHAIN_TO_LEAF + "-" + prefix
                disp_from_chain = CHAIN_FROM_LEAF + "-" + prefix
                to_upds = updates[disp_to_chain]
                from_upds = updates[disp_from_chain]
                to_deps = dependencies[disp_to_chain]
                from_deps = dependencies[disp_from_chain]
                new_leaf_chains.add(disp_from_chain)
                new_leaf_chains.add(disp_to_chain)
                # Root chain depends on its leaves.
                root_from_deps.add(disp_to_chain)
                root_to_deps.add(disp_from_chain)
                # Point root chain at prefix chain.
                iface_match = self.config.IFACE_PREFIX + prefix + "+"
                root_from_upds.append(
                    "--append %s --in-interface %s --goto %s" %
                    (CHAIN_FROM_ENDPOINT, iface_match, disp_from_chain)
                )
                root_to_upds.append(
                    "--append %s --out-interface %s --goto %s" %
                    (CHAIN_TO_ENDPOINT, iface_match, disp_to_chain)
                )

            # Common processing, add the per-endpoint rules to whichever
            # chain we decided above.
            for iface in interfaces:
                # Add rule to leaf or global chain to direct traffic to the
                # endpoint-specific one.  Note that we use --goto, which means
                # that the endpoint-specific chain will return to our parent
                # rather than to this chain.
                ep_suffix = interface_to_suffix(self.config, iface)
                to_chain_name, from_chain_name = chain_names(ep_suffix)
                from_upds.append("--append %s --in-interface %s --goto %s" %
                                 (disp_from_chain, iface, from_chain_name))
                from_deps.add(from_chain_name)
                to_upds.append("--append %s --out-interface %s --goto %s" %
                               (disp_to_chain, iface, to_chain_name))
                to_deps.add(to_chain_name)

            if not use_root_chain:
                # Add a default drop to the end of the leaf chain.
                from_upds.append("--append %s --jump DROP" % disp_from_chain)
                to_upds.append("--append %s --jump DROP" % disp_to_chain)

        # Both TO and FROM chains end with a DROP so that interfaces that
        # we don't know about yet can't bypass our rules.
        root_from_upds.append("--append %s --jump DROP" % CHAIN_FROM_ENDPOINT)
        root_to_upds.append("--append %s --jump DROP" % CHAIN_TO_ENDPOINT)
        chains_to_delete = self.programmed_leaf_chains - new_leaf_chains

        return chains_to_delete, dependencies, updates, new_leaf_chains