transdep/graph/analysis.go
2018-01-23 21:25:00 +01:00

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
}