478 lines
18 KiB
Go
478 lines
18 KiB
Go
package graph
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/hashicorp/go-immutable-radix"
|
|
"github.com/deckarep/golang-set"
|
|
"net"
|
|
"github.com/ANSSI-FR/transdep/tools"
|
|
)
|
|
|
|
/* simplifyRelWithCycle recursively visit the tree and bubbles up Cycle instances in AND Relations or removes them if
|
|
they are in OR Relations.
|
|
It also simplifies relation nodes with only one child by bubbling up the child.
|
|
|
|
This function returns true if the children list of the receiver was modified.
|
|
*/
|
|
func (rn *RelationshipNode) simplifyRelWithCycle() bool {
|
|
// newChildren is the list of children of the receiver after this function actions.
|
|
var newChildren []Node
|
|
modif := false
|
|
childrenToAnalyze := rn.children[:]
|
|
|
|
Outerloop:
|
|
for len(childrenToAnalyze) != 0 {
|
|
// mergedChildren will contain nodes contained in a child relation node which, itself, only has one child.
|
|
// For instance, if a node A has a child B, and B only child is C, then B is suppressed from A's children
|
|
// and C is added to mergedChildren.
|
|
var mergedChildren []Node
|
|
Innerloop:
|
|
for _, chld := range childrenToAnalyze {
|
|
if dg, ok := chld.(*RelationshipNode); ok {
|
|
// If the child node is a relation ship, visit the child recursively
|
|
modif = dg.simplifyRelWithCycle() || modif
|
|
// Now, if the child, after the recursive visit only has one child, bubble up that child
|
|
if len(dg.children) == 1 {
|
|
mergedChildren = append(mergedChildren, dg.children[0])
|
|
modif = true
|
|
// We continue, because this child node will not be added back to the children of the receiver
|
|
continue Innerloop
|
|
}
|
|
}
|
|
if _, ok := chld.(*Cycle); ok {
|
|
// Implicit: if the relation is not an AND, it is a OR. In OR relations, Cycles are a neutral element,
|
|
// like a 1 in a multiplicative expression.
|
|
if rn.relation == AND_REL && len(rn.children) > 1 {
|
|
// If the considered child is a Cycle and the receiver is an AND relation, then the receiver
|
|
// evaluation is summarized by this Cycle (because a Cycle in a AND relation is like a 0 in a
|
|
// multiplicative expression), so we just set the receiver's only child to a Cycle and don't process
|
|
// the remaining children.
|
|
newChildren = []Node{new(Cycle)}
|
|
modif = true
|
|
break Outerloop
|
|
}
|
|
}
|
|
// This node is not a Cycle, so we add it back as a child the receiver
|
|
newChildren = append(newChildren, chld)
|
|
}
|
|
// If we have bubbled up some grand-children nodes, we need to analyse them as children of the receiver
|
|
childrenToAnalyze = mergedChildren
|
|
}
|
|
rn.children = newChildren
|
|
return modif
|
|
}
|
|
|
|
/* auxSimplifyGraph recursively visits the graph and simplifies it. Simplification is done by merging relation
|
|
nodes when the receiver and one of its child relation node have the same relation type. Child relation nodes are like
|
|
parenthesis in a mathematical expression: 1 + (2*3 + 4) is equivalent to 1 + 2*3 + 4 and 2 * (3 * 4) is equivalent
|
|
to 2 * 3 * 4. Simplifying the graph that way reduces the depth of the graph and accelerates future visits.
|
|
|
|
This function returns true if the graph/tree below the receiver was altered
|
|
*/
|
|
func (rn *RelationshipNode) auxSimplifyGraph() bool {
|
|
var newChildren []Node
|
|
modif := false
|
|
|
|
// TODO I don't think I need to actually duplicate this
|
|
childrenToAnalyze := make([]Node, len(rn.children))
|
|
copy(childrenToAnalyze, rn.children)
|
|
|
|
for len(childrenToAnalyze) > 0 {
|
|
var mergedChildren []Node
|
|
for _, chldGraphNode := range childrenToAnalyze {
|
|
if chld, ok := chldGraphNode.(*RelationshipNode); ok {
|
|
if chld.relation == rn.relation {
|
|
// If the receiver's child currently considered is a RelationshipNode with the relation type as the
|
|
// receiver, then, add the children of this child node to the list of nodes that will be considered
|
|
// as children of the receiver.
|
|
mergedChildren = append(mergedChildren, chld.children...)
|
|
modif = true
|
|
} else {
|
|
// The child RelationshipNode node has a different relation type
|
|
// (AND containing an OR, or an OR containing an AND).
|
|
newChildren = append(newChildren, chldGraphNode)
|
|
}
|
|
} else {
|
|
// This child node is a LeafNode
|
|
newChildren = append(newChildren, chldGraphNode)
|
|
}
|
|
}
|
|
// TODO I don't think I need to actually duplicate this
|
|
childrenToAnalyze = make([]Node, len(mergedChildren))
|
|
copy(childrenToAnalyze, mergedChildren)
|
|
}
|
|
// TODO I don't think I need to actually duplicate this
|
|
rn.children = make([]Node, len(newChildren))
|
|
copy(rn.children, newChildren)
|
|
|
|
// Once the receiver simplified, we apply this function on all remaining children relation nodes
|
|
for _, chldGraphNode := range rn.children {
|
|
if chld, ok := chldGraphNode.(*RelationshipNode); ok {
|
|
modif = chld.auxSimplifyGraph() || modif
|
|
}
|
|
}
|
|
return modif
|
|
}
|
|
|
|
// SimplifyGraph creates a copy of the tree under the receiver, simplifies the radix under the copy, by applying
|
|
// repetitively auxSimplyGraph and simplifyRelWithCycle until the tree is stable.
|
|
// The copy is then returned.
|
|
func (rn *RelationshipNode) SimplifyGraph() *RelationshipNode {
|
|
ng, ok := rn.deepcopy().(*RelationshipNode)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
modif := true
|
|
for modif {
|
|
modif = false
|
|
modif = ng.auxSimplifyGraph() || modif
|
|
modif = ng.simplifyRelWithCycle() || modif
|
|
}
|
|
return ng
|
|
}
|
|
|
|
// buildLeafNodeInventory visits the tree under the receiver and returns the list of the LeafNodes. This list is built
|
|
// by visiting the tree recursively.
|
|
func (rn *RelationshipNode) buildLeafNodeInventory() []LeafNode {
|
|
l := make([]LeafNode, 0)
|
|
for _, absChld := range rn.children {
|
|
switch chld := absChld.(type) {
|
|
case *RelationshipNode:
|
|
l2 := chld.buildLeafNodeInventory()
|
|
l = append(l, l2...)
|
|
case LeafNode:
|
|
l = append(l, chld)
|
|
}
|
|
}
|
|
return l
|
|
}
|
|
|
|
// TODO add comment
|
|
func getSiblingsUsingSimilarity(leafNode LeafNode, inventory []LeafNode, breakV4, breakV6, DNSSECOnly bool) []LeafNode {
|
|
// siblings are leafNode that are considered unavailable during the analysis of leafNode
|
|
// Are considered unavailable other nodes that are similar to leafNode (similarity being defined by the similar()
|
|
// implementation of the leafNode underlying type. Are never considered unavailable unsigned names when DNSSECOnly
|
|
// is true as well as alias names. Alias names are always ignored because they are never the actual source of an
|
|
// unavailability; either the zone that contains the alias is unavailable or the zone containing the target of the
|
|
// alias is unavailable.
|
|
// IPv4 addresses are always considered unavailable if breakV4 is true. The same applies for IPv6 addresses w.r.t.
|
|
// breakV6.
|
|
var siblings []LeafNode
|
|
for _, node := range inventory {
|
|
toIgnore := false
|
|
toAdd := false
|
|
switch n := node.(type) {
|
|
case *DomainNameNode:
|
|
if DNSSECOnly && !n.DNSSECProtected() {
|
|
toIgnore = true
|
|
}
|
|
case *AliasNode:
|
|
toIgnore = true
|
|
case *IPNode:
|
|
isV4 := n.IsV4()
|
|
if (breakV4 && isV4) || (breakV6 && !isV4) {
|
|
toAdd = true
|
|
}
|
|
case *Cycle:
|
|
toAdd = true
|
|
}
|
|
if toAdd || (!toIgnore && leafNode.similar(node)) {
|
|
siblings = append(siblings, node)
|
|
}
|
|
}
|
|
return siblings
|
|
}
|
|
|
|
/* TODO revise this comment
|
|
testNodeCriticity returns true if leafNode is necessary for this tree to be resolved. External factors may influence
|
|
whether this leafNode is required to be available, including whether the IPv4 network or the IPv6 network are
|
|
available or whether we consider that only DNSSEC-protected zone may break (e.g. in case of invalid/expired
|
|
record signatures, or DS/DNSKEY algorithm mismatches) versus all zones (e.g. truncated zone, corrupted data, etc.).
|
|
|
|
leafNode is the node being tested
|
|
|
|
inventory is the list of all leafNodes that might be broken too and influence the result
|
|
|
|
breakV4, breakV6 and DNSSEConly are flags that indicates additional conditions for a node to be available or not.
|
|
*/
|
|
func (rn *RelationshipNode) testNodeCriticity(siblings []LeafNode) bool {
|
|
// The following loops purpose is to bubble up the unavailability markers of the leafNode. If an unavailable node
|
|
// is a child of an AND relationship, the whole relationship is unavailable. If an unavailable node is a child of
|
|
// an OR relationship, the whole relationship is unavailable if all of its children are unavailable.
|
|
// The algorithm terminates if the tree root is added to new unavailable node list or if there a no more
|
|
// unavailability markers that may bubble up.
|
|
// Since multiple "and" branches may have bubbling unavailability markers, "and"s bubble up only once, so that it
|
|
// does not mess up with the "or" count. "And"s bubbles up only once by marking it as "already bubbled". This is
|
|
// done by inserting it in the andSet. The number of children of an Or relationship that have bubbled up an
|
|
// unavailability marker is stored in the orSet variable.
|
|
orSet := make(map[*RelationshipNode]int)
|
|
andSet := make(map[*RelationshipNode]bool)
|
|
|
|
var unavailableNodes []Node
|
|
for _, n := range siblings {
|
|
unavailableNodes = append(unavailableNodes, n)
|
|
}
|
|
|
|
for len(unavailableNodes) > 0 {
|
|
nodesToHandle := unavailableNodes
|
|
unavailableNodes = []Node{}
|
|
|
|
for _, node := range nodesToHandle {
|
|
parent := node.parent()
|
|
if parent == nil {
|
|
// if "node" is the root node
|
|
return true
|
|
}
|
|
|
|
n := parent.(*RelationshipNode)
|
|
if n.relation == AND_REL {
|
|
if _, ok := andSet[n]; !ok {
|
|
andSet[n] = true
|
|
unavailableNodes = append(unavailableNodes, n)
|
|
}
|
|
} else {
|
|
if v, ok := orSet[n]; ok {
|
|
orSet[n] = v + 1
|
|
} else {
|
|
orSet[n] = 1
|
|
}
|
|
if len(n.children) == orSet[n] {
|
|
unavailableNodes = append(unavailableNodes, n)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TODO add comment
|
|
func getSiblingsByPrefixCloseness(n LeafNode, inventory []LeafNode) []LeafNode {
|
|
if ipn, ok := n.(*IPNode) ; ok {
|
|
return getSiblingsUsingFilteringFun(inventory, ipn.similarPrefix)
|
|
}
|
|
return []LeafNode{}
|
|
}
|
|
|
|
// TODO add comment
|
|
func (rn *RelationshipNode) findMandatoryNodesUsingPrefixCloseness(inventory []LeafNode)(mandatoryNodes, optionalNodes mapset.Set) {
|
|
return rn.findMandatoryNodes(inventory, getSiblingsByPrefixCloseness)
|
|
}
|
|
|
|
// TODO add comment
|
|
func (rn *RelationshipNode) findMandatoryNodesUsingSimilarity(inventory []LeafNode, breakV4, breakV6, DNSSECOnly bool) (mandatoryNodes, optionalNodes mapset.Set) {
|
|
getSiblingsFun := func(n LeafNode, inv []LeafNode) []LeafNode {
|
|
return getSiblingsUsingSimilarity(n, inv, breakV4, breakV6, DNSSECOnly)
|
|
}
|
|
return rn.findMandatoryNodes(inventory, getSiblingsFun)
|
|
}
|
|
|
|
// TODO add comment
|
|
func getSiblingsUsingFilteringFun(inventory []LeafNode, customFilterFun func(lf LeafNode) bool) []LeafNode {
|
|
var siblings []LeafNode
|
|
for _, lf := range inventory {
|
|
if _, ok := lf.(*Cycle) ; ok || customFilterFun(lf) {
|
|
siblings = append(siblings, lf)
|
|
}
|
|
}
|
|
return siblings
|
|
}
|
|
|
|
// TODO add comment
|
|
func getSiblingsByASN(n LeafNode, inventory []LeafNode) []LeafNode {
|
|
if ipn, ok := n.(*IPNode) ; ok {
|
|
return getSiblingsUsingFilteringFun(inventory, ipn.similarASN)
|
|
}
|
|
return []LeafNode{}
|
|
}
|
|
|
|
// TODO add comment
|
|
func (rn *RelationshipNode) findMandatoryNodesUsingASN(inventory []LeafNode) (mandatoryNodes, optionalNodes mapset.Set) {
|
|
return rn.findMandatoryNodes(inventory, getSiblingsByASN)
|
|
}
|
|
|
|
// TODO revise comment
|
|
// findMandatoryNodes explores all nodes from the inventory and returns the list of leafNodes that are mandatory
|
|
func (rn *RelationshipNode) findMandatoryNodes(inventory []LeafNode, getSiblingsFun func(LeafNode, []LeafNode) []LeafNode) (mandatoryNodes, optionalNodes mapset.Set) {
|
|
mandatoryNodesSet := make(map[[8]byte]LeafNode)
|
|
optionalNodesSet := make(map[[8]byte]LeafNode)
|
|
|
|
for _, leafNode := range inventory {
|
|
// We use a hash of the leafNode to "uniquely" identify nodes. This is because several leafNode instances have
|
|
// different memory addresses, while still representing the same node (at the semantic level).
|
|
h := leafNode.hash()
|
|
|
|
// Test whether this node was already evaluated
|
|
if _, ok := mandatoryNodesSet[h]; ok {
|
|
continue
|
|
}
|
|
if _, ok := optionalNodesSet[h]; ok {
|
|
continue
|
|
}
|
|
|
|
// We cut the inventory using pos, because if we are here, all previous nodes were not "siblings" of this one.
|
|
// If they had been, we would have "continue"d during the previous tests
|
|
siblings := getSiblingsFun(leafNode, inventory)
|
|
if rn.testNodeCriticity(siblings) {
|
|
mandatoryNodesSet[h] = leafNode
|
|
} else {
|
|
optionalNodesSet[h] = leafNode
|
|
}
|
|
}
|
|
|
|
mandatoryNodes = mapset.NewThreadUnsafeSet()
|
|
optionalNodes = mapset.NewThreadUnsafeSet()
|
|
// Convert the map into a list of the map values
|
|
for _, v := range mandatoryNodesSet {
|
|
mandatoryNodes.Add(v)
|
|
}
|
|
// Convert the map into a list of the map values
|
|
for _, v := range optionalNodesSet {
|
|
optionalNodes.Add(v)
|
|
}
|
|
return mandatoryNodes, optionalNodes
|
|
}
|
|
|
|
// TODO add comment
|
|
func convertToListOfLeafNodes(s mapset.Set) []LeafNode {
|
|
var l []LeafNode
|
|
for _, v := range s.ToSlice() {
|
|
l = append(l, v.(LeafNode))
|
|
}
|
|
return l
|
|
}
|
|
|
|
// analyse starts the analysis of the tree under the receiver and returns the list of mandatory nodes
|
|
func (rn *RelationshipNode) analyse(breakV4, breakV6, DNSSECOnly bool, tree *iradix.Tree) []CriticalNode {
|
|
// A copy of the receiver's tree is performed because we will alter the nodes by simplyfing the graph and setting
|
|
// the parent of the nodes and we don't want to be "destructive" in anyway
|
|
ng := rn.SimplifyGraph()
|
|
ng.setParentNodes()
|
|
|
|
inventory := ng.buildLeafNodeInventory()
|
|
|
|
var criticalNodes []CriticalNode
|
|
|
|
mandatoryNodes, _ := ng.findMandatoryNodesUsingSimilarity(inventory, breakV4, breakV6, DNSSECOnly)
|
|
for _, node := range convertToListOfLeafNodes(mandatoryNodes) {
|
|
switch typedNode := node.(type) {
|
|
case *DomainNameNode:
|
|
criticalNodes = append(criticalNodes, CriticalName{typedNode.Domain()})
|
|
case *IPNode:
|
|
criticalNodes = append(criticalNodes, CriticalIP{net.ParseIP(typedNode.IP())})
|
|
case *Cycle:
|
|
criticalNodes = append(criticalNodes, &Cycle{})
|
|
}
|
|
}
|
|
|
|
mandatoryNodes, _ = ng.findMandatoryNodesUsingASN(inventory)
|
|
asnSet := make(map[int]bool)
|
|
for _, node := range convertToListOfLeafNodes(mandatoryNodes) {
|
|
if typedNode, ok := node.(*IPNode) ; ok {
|
|
asnSet[typedNode.ASN()] = true
|
|
}
|
|
}
|
|
for asn, _ := range asnSet {
|
|
criticalNodes = append(criticalNodes, CriticalASN{asn})
|
|
}
|
|
|
|
mandatoryNodes, _ = ng.findMandatoryNodesUsingPrefixCloseness(inventory)
|
|
prefixSet := make(map[string]bool)
|
|
for _, node := range convertToListOfLeafNodes(mandatoryNodes) {
|
|
if typedNode, ok := node.(*IPNode) ; ok {
|
|
prefixSet[typedNode.Prefix()] = true
|
|
}
|
|
}
|
|
for prefix, _ := range prefixSet {
|
|
criticalNodes = append(criticalNodes, CriticalPrefix{net.ParseIP(prefix)})
|
|
}
|
|
return criticalNodes
|
|
}
|
|
|
|
// Analyse is the exported version of analyse. It starts the analysis of the tree under the receiver and returns the
|
|
// list of mandatory nodes.
|
|
// IPv4 and IPv6 addresses have normal availability markers (no breakV4/breakV6)
|
|
func (rn *RelationshipNode) Analyse(DNSSECOnly bool, tree *iradix.Tree) []CriticalNode {
|
|
return rn.analyse(false, false, DNSSECOnly, tree)
|
|
}
|
|
|
|
// AnalyseWithoutV4 runs the same type of analysis as "Analyse" except all IPv4 addresses are marked as unavailable.
|
|
// This may reveal that some IPv6 are actually SPOFs when IPv4 addresses are not available.
|
|
// AnalyseWithoutV4 may either return the list of mandatory leafNodes or an error if the name cannot be resolved without
|
|
// IPv4 address participation.
|
|
func (rn *RelationshipNode) AnalyseWithoutV4(DNSSECOnly bool, tree *iradix.Tree) ([]CriticalNode, error) {
|
|
l := rn.analyse(true, false, DNSSECOnly, tree)
|
|
for _, e := range l {
|
|
if node, ok := e.(CriticalIP); ok {
|
|
if node.IP.To4() != nil {
|
|
return []CriticalNode{}, fmt.Errorf("this domain name requires some IPv4 addresses to be resolved properly")
|
|
}
|
|
}
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
// AnalyseWithoutV6 runs the same type of analysis as "Analyse" except all IPv6 addresses are marked as unavailable.
|
|
// This may reveal that some IPv4 are actually SPOFs when IPv6 addresses are not available.
|
|
// AnalyseWithoutV6 may either return the list of mandatory leafNodes or an error if the name cannot be resolved without
|
|
// IPv6 address participation.
|
|
func (rn *RelationshipNode) AnalyseWithoutV6(DNSSECOnly bool, tree *iradix.Tree) ([]CriticalNode, error) {
|
|
l := rn.analyse(false, true, DNSSECOnly, tree)
|
|
for _, e := range l {
|
|
if node, ok := e.(CriticalIP); ok {
|
|
if node.IP.To4() == nil {
|
|
return []CriticalNode{}, fmt.Errorf("this domain name requires some IPv6 addresses to be resolved properly")
|
|
}
|
|
}
|
|
}
|
|
return l, nil
|
|
}
|
|
|
|
|
|
type WorkerAnalysisResult struct {
|
|
Nodes []CriticalNode
|
|
Err error
|
|
}
|
|
|
|
func PerformAnalyseOnResult(g *RelationshipNode, reqConf *tools.RequestConfig, tree *iradix.Tree) (allNamesResult, allNamesNo4Result, allNamesNo6Result, dnssecResult, dnssecNo4Result, dnssecNo6Result *WorkerAnalysisResult) {
|
|
if !reqConf.AnalysisCond.DNSSEC || reqConf.AnalysisCond.All {
|
|
dnssecResult = nil
|
|
dnssecNo4Result = nil
|
|
dnssecNo6Result = nil
|
|
allNamesResult, allNamesNo4Result, allNamesNo6Result = performAnalyseOnResultWithDNSSECIndicator(g, reqConf,false, tree)
|
|
}
|
|
|
|
if reqConf.AnalysisCond.DNSSEC || reqConf.AnalysisCond.All {
|
|
if !reqConf.AnalysisCond.All {
|
|
allNamesResult = nil
|
|
allNamesNo4Result = nil
|
|
allNamesNo6Result = nil
|
|
}
|
|
dnssecResult, dnssecNo4Result, dnssecNo6Result = performAnalyseOnResultWithDNSSECIndicator(g, reqConf, true, tree)
|
|
}
|
|
return
|
|
}
|
|
|
|
func performAnalyseOnResultWithDNSSECIndicator(g *RelationshipNode, reqConf *tools.RequestConfig, DNSSEC bool, tree *iradix.Tree) (natural, noV4, noV6 *WorkerAnalysisResult) {
|
|
if reqConf.AnalysisCond.All || (!reqConf.AnalysisCond.NoV4 && !reqConf.AnalysisCond.NoV6) {
|
|
natural = &WorkerAnalysisResult{g.Analyse(DNSSEC, tree), nil}
|
|
} else {
|
|
natural = nil
|
|
}
|
|
|
|
if reqConf.AnalysisCond.All || reqConf.AnalysisCond.NoV4 {
|
|
analyseResult, err := g.AnalyseWithoutV4(DNSSEC, tree)
|
|
noV4 = &WorkerAnalysisResult{analyseResult, err}
|
|
} else {
|
|
noV4 = nil
|
|
}
|
|
|
|
if reqConf.AnalysisCond.All || reqConf.AnalysisCond.NoV6 {
|
|
analyseResult, err := g.AnalyseWithoutV6(DNSSEC, tree)
|
|
noV6 = &WorkerAnalysisResult{analyseResult, err}
|
|
} else {
|
|
noV6 = nil
|
|
}
|
|
return
|
|
}
|
|
|