316 lines
12 KiB
Go
316 lines
12 KiB
Go
|
package dependency
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"github.com/hashicorp/go-immutable-radix"
|
||
|
"github.com/miekg/dns"
|
||
|
"github.com/ANSSI-FR/transdep/graph"
|
||
|
"github.com/ANSSI-FR/transdep/messages/dependency"
|
||
|
"github.com/ANSSI-FR/transdep/messages/nameresolver"
|
||
|
"github.com/ANSSI-FR/transdep/messages/zonecut"
|
||
|
"github.com/ANSSI-FR/transdep/tools"
|
||
|
"github.com/ANSSI-FR/transdep/tools/radix"
|
||
|
"github.com/ANSSI-FR/transdep/errors"
|
||
|
)
|
||
|
|
||
|
const WORKER_CHAN_CAPACITY = 10
|
||
|
|
||
|
// worker represents a handler of requests for a specific requestTopic.
|
||
|
// It retrieves the relevant information, cache it in memory and serves it until stop() is called.
|
||
|
type worker struct {
|
||
|
// req is the request that is handled by this worker
|
||
|
req *dependency.Request
|
||
|
// reqs is a channel of requests with identical requestTopic as the original request
|
||
|
reqs chan *dependency.Request
|
||
|
// joinChan is used by stop() to wait for the completion of the start() goroutine
|
||
|
joinChan chan bool
|
||
|
// closedReqChan is used to prevent double-close during stop()
|
||
|
closedReqChan bool
|
||
|
// tree is the reference to a radix tree containing a view of the prefixes announced with BGP over the Internet.
|
||
|
// This is used to fill IPNode instances with their corresponding ASN number, at the time of query.
|
||
|
tree *iradix.Tree
|
||
|
// depHandler is the handler used to fetch the dependency tree of a dependency of the current requestTopic
|
||
|
depHandler func(*dependency.Request) *errors.ErrorStack
|
||
|
// zcHandler is used to get the delegation info of some name that is part of the dependency tree of the current requestTopic
|
||
|
zcHandler func(request *zonecut.Request) *errors.ErrorStack
|
||
|
// nrHandler is used to get the IP addresses or Alias associated to a name that is part of the dependency tree of the current requestTopic
|
||
|
nrHandler func(*nameresolver.Request) *errors.ErrorStack
|
||
|
// config is the configuration of the current Transdep run
|
||
|
config *tools.TransdepConfig
|
||
|
}
|
||
|
|
||
|
/* newWorker instantiates and returns a new worker.
|
||
|
It builds the worker struct, and starts the routine in charge of building the dependency tree of the
|
||
|
requested topic and serving the answer to subsequent requests.
|
||
|
|
||
|
req is the first request that triggered the instantiation of that worker
|
||
|
|
||
|
depHandler is a function that can be called to have another dependency graph resolved (probably to integrate it to the current one)
|
||
|
|
||
|
zcHandler is a function that can be called to obtain the zone cut of a requested name
|
||
|
|
||
|
nrHandler is a function that can be called to obtain the IP address or Alias of a name
|
||
|
*/
|
||
|
func newWorker(req *dependency.Request, depHandler func(*dependency.Request) *errors.ErrorStack, zcHandler func(request *zonecut.Request) *errors.ErrorStack, nrHandler func(*nameresolver.Request) *errors.ErrorStack, conf *tools.TransdepConfig, tree *iradix.Tree) *worker {
|
||
|
w := new(worker)
|
||
|
w.req = req
|
||
|
|
||
|
w.reqs = make(chan *dependency.Request, WORKER_CHAN_CAPACITY)
|
||
|
w.closedReqChan = false
|
||
|
|
||
|
w.joinChan = make(chan bool, 1)
|
||
|
|
||
|
w.config = conf
|
||
|
|
||
|
w.tree = tree
|
||
|
|
||
|
w.depHandler = depHandler
|
||
|
w.zcHandler = zcHandler
|
||
|
w.nrHandler = nrHandler
|
||
|
|
||
|
w.start()
|
||
|
return w
|
||
|
}
|
||
|
|
||
|
/* handle is the function called to submit a new request to that worker.
|
||
|
Caller may call req.Result() after this function returns to get the result for this request.
|
||
|
This method returns an error if the worker is stopped or if the submitted request does not match the request usually
|
||
|
handled by this worker.
|
||
|
*/
|
||
|
func (w *worker) handle(req *dependency.Request) *errors.ErrorStack {
|
||
|
if w.closedReqChan {
|
||
|
return errors.NewErrorStack(fmt.Errorf("handle: dependency worker channel for %s is already closed", w.req.Name()))
|
||
|
} else if !w.req.Equal(req) {
|
||
|
return errors.NewErrorStack(fmt.Errorf("handle: invalid request; the submitted request (%s) does not match the requests handled by this worker (%s)", req.Name(), w.req.Name()))
|
||
|
}
|
||
|
w.reqs <- req
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// resolveRoot is a trick used to simplify the circular dependency of the root-zone, which is self-sufficient by definition.
|
||
|
func (w *worker) resolveRoot() graph.Node {
|
||
|
g := graph.NewRelationshipNode("resolveRoot: dependency graph of the root zone", graph.AND_REL)
|
||
|
g.AddChild(graph.NewDomainNameNode(".", true))
|
||
|
return g
|
||
|
}
|
||
|
|
||
|
/*getParentGraph is a helper function which gets the dependency graph of the parent domain.
|
||
|
This function submits a new dependency request for the parent domain and waits for the result.
|
||
|
Consequently, this function triggers a recursive search of the parent domain dependency tree until the root-zone
|
||
|
dependency tree is reached. Said otherwise, for "toto.fr", this function triggers a search for the dependency radix of
|
||
|
"fr.", which will recursively trigger a search for the dependency tree of ".".
|
||
|
*/
|
||
|
func (w *worker) getParentGraph() (graph.Node, *errors.ErrorStack) {
|
||
|
nxtLblPos, end := dns.NextLabel(w.req.Name(), 1)
|
||
|
shrtndName := "."
|
||
|
if !end {
|
||
|
shrtndName = w.req.Name()[nxtLblPos:]
|
||
|
}
|
||
|
|
||
|
// resolveName and includeIP are set to false, because this name is not dependent of the IP address of set at the
|
||
|
// parent domain, and we are not compatible with DNAME.
|
||
|
req := dependency.NewRequestWithContext(shrtndName, false, false, w.req, 0)
|
||
|
|
||
|
w.depHandler(req)
|
||
|
|
||
|
res, err := req.Result()
|
||
|
if err != nil {
|
||
|
err.Push(fmt.Errorf("getParentGraph: error during resolution of parent graph %s of %s", shrtndName, w.req.Name()))
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return res, nil
|
||
|
}
|
||
|
|
||
|
// resolveSelf returns the graph of the current requestTopic
|
||
|
func (w *worker) resolveSelf() (graph.Node, *errors.ErrorStack) {
|
||
|
g := graph.NewRelationshipNode(fmt.Sprintf("Dependency graph of exact name %s", w.req.Name()), graph.AND_REL)
|
||
|
|
||
|
// First, we resolve the current name, to get its IP addresses or the indication that it is an alias
|
||
|
nr := nameresolver.NewRequest(w.req.Name(), w.req.Exceptions())
|
||
|
w.nrHandler(nr)
|
||
|
|
||
|
var ne *nameresolver.Entry
|
||
|
ne, err := nr.Result()
|
||
|
if err != nil {
|
||
|
err.Push(fmt.Errorf("resolveSelf: error while getting the exact resolution of %s", w.req.Name()))
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if ne.CNAMETarget() != "" {
|
||
|
if !w.req.FollowAlias() {
|
||
|
return nil, errors.NewErrorStack(fmt.Errorf("resolveSelf: alias detected (%s) but alias is not requested to be added to the graph of %s", ne.CNAMETarget(), w.req.Name()))
|
||
|
}
|
||
|
// the following line is commented because we might not want to add to the dependency graph the name of the node
|
||
|
// that contains an alias that draws in a complete dependency graph, because this name is not really important
|
||
|
// per se wrt dependency graphs.
|
||
|
g.AddChild(graph.NewAliasNode(ne.CNAMETarget(), ne.Owner()))
|
||
|
|
||
|
// We reuse the FollowAlias and IncludeIP value of the current requestTopic because if we are resolving a
|
||
|
// name for a NS, we will want the IP address and to follow CNAMEs, even though this is an illegal configuration.
|
||
|
// Depth is incremented so that overly long chains can be detected
|
||
|
depReq := dependency.NewRequestWithContext(ne.CNAMETarget(), w.req.FollowAlias(), w.req.IncludeIP(), w.req, w.req.Depth()+1)
|
||
|
w.depHandler(depReq)
|
||
|
|
||
|
aliasGraph, err := depReq.Result()
|
||
|
if err != nil {
|
||
|
err.Push(fmt.Errorf("resolveSelf: error while getting the dependency graph of alias %s", ne.CNAMETarget()))
|
||
|
return nil, err
|
||
|
}
|
||
|
g.AddChild(aliasGraph)
|
||
|
} else if w.req.IncludeIP() {
|
||
|
gIP := graph.NewRelationshipNode(fmt.Sprintf("IPs of %s", ne.Owner()), graph.OR_REL)
|
||
|
g.AddChild(gIP)
|
||
|
for _, addr := range ne.Addrs() {
|
||
|
asn, err := radix.GetASNFor(w.tree, addr)
|
||
|
if err != nil {
|
||
|
asn = 0
|
||
|
}
|
||
|
gIP.AddChild(graph.NewIPNodeWithName(addr.String(), ne.Owner(), asn))
|
||
|
}
|
||
|
}
|
||
|
return g, nil
|
||
|
}
|
||
|
|
||
|
// getDelegationGraph gets the graph relative to the delegation info of the current name. The graph is empty if the
|
||
|
// request topic is not a zone apex.
|
||
|
func (w *worker) getDelegationGraph() (graph.Node, *errors.ErrorStack) {
|
||
|
g := graph.NewRelationshipNode(fmt.Sprintf("Dependency graph for %s delegation", w.req.Name()), graph.AND_REL)
|
||
|
// Get the graph for the current zone. First, we get the delegation info for this zone, and we add it.
|
||
|
req := zonecut.NewRequest(w.req.Name(), w.req.Exceptions())
|
||
|
w.zcHandler(req)
|
||
|
|
||
|
entry, err := req.Result()
|
||
|
if err != nil {
|
||
|
var returnErr bool
|
||
|
switch typedErr := err.OriginalError().(type) {
|
||
|
case *errors.TimeoutError:
|
||
|
returnErr = true
|
||
|
case *errors.NXDomainError:
|
||
|
returnErr = w.req.Exceptions().RFC8020
|
||
|
case *errors.ServfailError:
|
||
|
returnErr = !w.req.Exceptions().AcceptServFailAsNoData
|
||
|
case *errors.NoNameServerError:
|
||
|
returnErr = false
|
||
|
default:
|
||
|
_ = typedErr
|
||
|
returnErr = true
|
||
|
}
|
||
|
if returnErr {
|
||
|
err.Push(fmt.Errorf("getDelegationGraph: error while getting the zone cut of %s", w.req.Name()))
|
||
|
return nil, err
|
||
|
}
|
||
|
err = nil
|
||
|
entry = nil
|
||
|
}
|
||
|
|
||
|
// If entry is nil, then we are at a non-terminal node, so we have no other dependencies (except aliases)
|
||
|
if entry != nil {
|
||
|
g.AddChild(graph.NewDomainNameNode(entry.Domain(), entry.DNSSEC()))
|
||
|
|
||
|
nameSrvsGraph := graph.NewRelationshipNode(fmt.Sprintf("Graph of NameSrvInfo of %s", w.req.Name()), graph.OR_REL)
|
||
|
g.AddChild(nameSrvsGraph)
|
||
|
for _, nameSrv := range entry.NameServers() {
|
||
|
nsGraph := graph.NewRelationshipNode(fmt.Sprintf("Graph of NS %s from NameSrvInfo of %s", nameSrv.Name(), w.req.Name()), graph.AND_REL)
|
||
|
nameSrvsGraph.AddChild(nsGraph)
|
||
|
|
||
|
// If there are glues
|
||
|
if len(nameSrv.Addrs()) > 0 {
|
||
|
nsAddrGraph := graph.NewRelationshipNode(fmt.Sprintf("IPs of %s", nameSrv.Name()), graph.OR_REL)
|
||
|
nsGraph.AddChild(nsAddrGraph)
|
||
|
for _, ip := range nameSrv.Addrs() {
|
||
|
asn, err := radix.GetASNFor(w.tree, ip)
|
||
|
if err != nil {
|
||
|
asn = 0
|
||
|
}
|
||
|
nsAddrGraph.AddChild(graph.NewIPNodeWithName(ip.String(), nameSrv.Name(), asn))
|
||
|
}
|
||
|
} else {
|
||
|
// The NS is out-of-bailiwick and does not contain glues; thus we ask for the dependency graph of
|
||
|
// this NS name res
|
||
|
req := dependency.NewRequestWithContext(nameSrv.Name(), true, true, w.req, 0)
|
||
|
w.depHandler(req)
|
||
|
|
||
|
NSGraph, err := req.Result()
|
||
|
if err != nil {
|
||
|
err.Push(fmt.Errorf("getDelegationGraph: error while getting the dependency graph of NS %s", nameSrv.Name()))
|
||
|
return nil, err
|
||
|
}
|
||
|
nsGraph.AddChild(NSGraph)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return g, nil
|
||
|
}
|
||
|
|
||
|
// resolve orchestrates the resolution of the worker request topic and returns it
|
||
|
func (w *worker) resolve() (graph.Node, *errors.ErrorStack) {
|
||
|
// Shortcut if this is the root zone, because we don't want to have to handle the circular dependency of the root-zone
|
||
|
if w.req.Name() == "." {
|
||
|
g := w.resolveRoot()
|
||
|
return g, nil
|
||
|
}
|
||
|
|
||
|
// The graph of a name is the graph of the parent name + the graph of the name in itself (including its eventual
|
||
|
// delegation info and its eventual alias/IP address)
|
||
|
g := graph.NewRelationshipNode(fmt.Sprintf("Dependency graph for %s", w.req.Name()), graph.AND_REL)
|
||
|
|
||
|
// Get Graph of the parent zone
|
||
|
res, err := w.getParentGraph()
|
||
|
if err != nil {
|
||
|
err.Push(fmt.Errorf("resolve: error while getting the parent graph of %s", w.req.Name()))
|
||
|
return nil, err
|
||
|
}
|
||
|
g.AddChild(res)
|
||
|
|
||
|
// Get Graph of the delegation of the request topic
|
||
|
graphDelegRes, err := w.getDelegationGraph()
|
||
|
if err != nil {
|
||
|
err.Push(fmt.Errorf("resolve: error while getting the delegation graph of %s", w.req.Name()))
|
||
|
return nil, err
|
||
|
}
|
||
|
g.AddChild(graphDelegRes)
|
||
|
|
||
|
// If the request topic is interesting in itself (for instance, because it is the name used in a NS record and that
|
||
|
// name is out-of-bailiwick), we resolve its graph and add it
|
||
|
if w.req.ResolveTargetName() {
|
||
|
res, err := w.resolveSelf()
|
||
|
if err != nil {
|
||
|
err.Push(fmt.Errorf("resolve: error while resolving %s", w.req.Name()))
|
||
|
return nil, err
|
||
|
}
|
||
|
g.AddChild(res)
|
||
|
}
|
||
|
|
||
|
return g, nil
|
||
|
}
|
||
|
|
||
|
// start launches a goroutine in charge of resolving the request topic, and then serving the result of this resolution
|
||
|
// to subsequent identical request topic
|
||
|
func (w *worker) start() {
|
||
|
go func() {
|
||
|
result, err := w.resolve()
|
||
|
if err != nil {
|
||
|
result = nil
|
||
|
err.Push(fmt.Errorf("start: error while resolving dependency graph of %s", w.req.Name()))
|
||
|
}
|
||
|
for req := range w.reqs {
|
||
|
req.SetResult(result, err)
|
||
|
}
|
||
|
w.joinChan <- true
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
// stop is to be called during the cleanup of the worker. It shuts down the goroutine started by start() and waits for
|
||
|
// it to actually end.
|
||
|
func (w *worker) stop() bool {
|
||
|
if w.closedReqChan {
|
||
|
return false
|
||
|
}
|
||
|
close(w.reqs)
|
||
|
w.closedReqChan = true
|
||
|
<-w.joinChan
|
||
|
close(w.joinChan)
|
||
|
return true
|
||
|
}
|