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

396 lines
13 KiB
Go

package main
import (
"bufio"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"github.com/ANSSI-FR/transdep/dependency"
dep_msg "github.com/ANSSI-FR/transdep/messages/dependency"
"github.com/ANSSI-FR/transdep/graph"
"strings"
"github.com/hashicorp/go-immutable-radix"
"github.com/ANSSI-FR/transdep/tools/radix"
"github.com/ANSSI-FR/transdep/tools"
)
func displayDomain(prefix string, res *graph.WorkerAnalysisResult, conf *tools.RequestConfig) {
if res.Err != nil {
if conf.OutputFormat.ScriptFriendlyOutput {
fmt.Printf("%s%s\n", prefix, "-ERROR-")
} else {
fmt.Printf("%s%s\n", prefix, res.Err)
}
} else {
for _, elmt := range res.Nodes {
switch e := elmt.(type) {
case graph.CriticalName:
fmt.Printf("%sName:%s\n", prefix, e.Name)
case graph.CriticalAlias:
fmt.Printf("%sAlias:%s->%s\n", prefix, e.Source, e.Target)
case graph.CriticalIP:
fmt.Printf("%sIP:%s\n", prefix, e.IP.String())
case graph.CriticalASN:
fmt.Printf("%sASN:%d\n", prefix, e.ASN)
case graph.CriticalPrefix:
if e.Prefix.To4() != nil {
fmt.Printf("%sPrefix:%s/24\n", prefix, e.Prefix.String())
} else {
fmt.Printf("%sPrefix:%s/48\n", prefix, e.Prefix.String())
}
case *graph.Cycle:
fmt.Printf("%sCycle\n", prefix)
default:
panic("BUG: missing case")
}
}
}
}
type WorkerResult struct {
dn string
stringRepr string
allNames *graph.WorkerAnalysisResult
dnssec *graph.WorkerAnalysisResult
allNamesNo4 *graph.WorkerAnalysisResult
dnssecNo4 *graph.WorkerAnalysisResult
allNamesNo6 *graph.WorkerAnalysisResult
dnssecNo6 *graph.WorkerAnalysisResult
err error
}
func performBackgroundAnalysis(name string, g *graph.RelationshipNode, ansChan chan<- *WorkerResult, analysisDoneChan chan<- bool, requestConf *tools.RequestConfig, tree *iradix.Tree) {
allNamesResult, allNamesNo4Result, allNamesNo6Result, dnssecResult, dnssecNo4Result, dnssecNo6Result := graph.PerformAnalyseOnResult(g, requestConf, tree)
ansChan <- &WorkerResult{
name, "",
allNamesResult,
dnssecResult,
allNamesNo4Result,
dnssecNo4Result,
allNamesNo6Result,
dnssecNo6Result,
nil,
}
analysisDoneChan <- true
}
func spoolDependencyRequest(wc <-chan *dep_msg.Request, ansChan chan<- *WorkerResult, df *dependency.Finder, reqConf *tools.RequestConfig, transdepConf *tools.TransdepConfig, tree *iradix.Tree) {
currentlyAnalyzedCounter := 0
analysisDoneChan := make(chan bool, transdepConf.JobCount)
inputClosed := false
for !inputClosed || currentlyAnalyzedCounter != 0 {
select {
case _ = <- analysisDoneChan:
currentlyAnalyzedCounter--
case req, opened := <-wc:
inputClosed = !opened
if req != nil && opened {
if err := df.Handle(req) ; err != nil {
ansChan <- &WorkerResult{
req.Name(), "",nil, nil,
nil, nil, nil, nil,
err,
}
}
res, err := req.Result()
if err != nil {
ansChan <- &WorkerResult{
req.Name(), "", nil, nil,
nil, nil, nil, nil,
err,
}
} else {
relNode, ok := res.(*graph.RelationshipNode)
if !ok {
ansChan <- &WorkerResult{
req.Name(), "", nil, nil,
nil, nil, nil, nil,
fmt.Errorf("returned node is not a RelationshipNode instance"),
}
}
if reqConf.OutputFormat.Graph {
jsonbstr, err := json.Marshal(relNode.SimplifyGraph())
ansChan <- &WorkerResult{
req.Name(), string(jsonbstr), nil, nil,
nil, nil, nil, nil,
err,
}
} else if reqConf.OutputFormat.DotOutput {
ansChanForDot := make(chan *WorkerResult, 1)
analysisDoneChanForDot := make(chan bool, 1)
go performBackgroundAnalysis(req.Name(), relNode, ansChanForDot, analysisDoneChanForDot, reqConf, tree)
<- analysisDoneChanForDot
analysisResult := <- ansChanForDot
var criticalNodes []graph.CriticalNode
if reqConf.AnalysisCond.DNSSEC {
if reqConf.AnalysisCond.NoV4 {
criticalNodes = analysisResult.dnssecNo4.Nodes
} else if reqConf.AnalysisCond.NoV6 {
criticalNodes = analysisResult.dnssecNo6.Nodes
} else {
criticalNodes = analysisResult.dnssec.Nodes
}
} else if reqConf.AnalysisCond.NoV4 {
criticalNodes = analysisResult.allNamesNo4.Nodes
} else if reqConf.AnalysisCond.NoV6 {
criticalNodes = analysisResult.allNamesNo6.Nodes
} else {
criticalNodes = analysisResult.allNames.Nodes
}
g, _ := graph.DrawGraph(relNode.SimplifyGraph(), criticalNodes)
g.SetStrict(true)
ansChan <- &WorkerResult{
req.Name(), g.String(), nil, nil,
nil, nil, nil, nil,
err,
}
} else {
go performBackgroundAnalysis(req.Name(), relNode, ansChan, analysisDoneChan, reqConf, tree)
currentlyAnalyzedCounter++
}
}
}
}
}
ansChan <- nil
}
func handleWorkerResponse(res *WorkerResult, reqConf *tools.RequestConfig) bool {
if res == nil {
return true
}
if res.err != nil {
if reqConf.OutputFormat.ScriptFriendlyOutput {
fmt.Printf("Error:%s:%s\n", res.dn, "-FAILURE-")
} else {
fmt.Printf("Error:%s:%s\n", res.dn, fmt.Sprintf("Error while resolving this name: %s", res.err))
}
} else if reqConf.OutputFormat.Graph {
fmt.Printf("%s:%s\n", res.dn, res.stringRepr)
} else if reqConf.OutputFormat.DotOutput {
fmt.Println(res.stringRepr)
} else {
if reqConf.AnalysisCond.All {
displayDomain(fmt.Sprintf("AllNames:%s:", res.dn), res.allNames, reqConf)
displayDomain(fmt.Sprintf("DNSSEC:%s:", res.dn), res.dnssec, reqConf)
displayDomain(fmt.Sprintf("AllNamesNo4:%s:", res.dn), res.allNamesNo4, reqConf)
displayDomain(fmt.Sprintf("DNSSECNo4:%s:", res.dn), res.dnssecNo4, reqConf)
displayDomain(fmt.Sprintf("AllNamesNo6:%s:", res.dn), res.allNamesNo6, reqConf)
displayDomain(fmt.Sprintf("DNSSECNo6:%s:", res.dn), res.dnssecNo6, reqConf)
} else if reqConf.AnalysisCond.DNSSEC {
if reqConf.AnalysisCond.NoV4 {
displayDomain(fmt.Sprintf("%s:", res.dn), res.dnssecNo4, reqConf)
} else if reqConf.AnalysisCond.NoV6 {
displayDomain(fmt.Sprintf("%s:", res.dn), res.dnssecNo6, reqConf)
} else {
displayDomain(fmt.Sprintf("%s:", res.dn), res.dnssec, reqConf)
}
} else {
if reqConf.AnalysisCond.NoV4 {
displayDomain(fmt.Sprintf("%s:", res.dn), res.allNamesNo4, reqConf)
} else if reqConf.AnalysisCond.NoV6 {
displayDomain(fmt.Sprintf("%s:", res.dn), res.allNamesNo6, reqConf)
} else {
displayDomain(fmt.Sprintf("%s:", res.dn), res.allNames, reqConf)
}
}
}
return false
}
func createDomainNameStreamer(fileName string, c chan<- string) {
fd, err := os.Open(fileName)
if err != nil {
panic("Unable to open file for read access")
}
reader := bufio.NewReader(fd)
err = nil
for err == nil {
var line string
line, err = reader.ReadString('\n')
if err != nil {
if err != io.EOF {
panic("Error while reading file")
}
}
c <- strings.TrimRight(line, "\n")
}
close(c)
}
func analyseDomains(domainNameChan <-chan string, reqConf *tools.RequestConfig, transdepConf *tools.TransdepConfig, df *dependency.Finder, tree *iradix.Tree) {
// Start workers
wc := make(chan *dep_msg.Request)
ansChan := make(chan *WorkerResult, 1)
for i := 0; i < transdepConf.JobCount; i++ {
go spoolDependencyRequest(wc, ansChan, df, reqConf, transdepConf, tree)
}
// Prepare for reading input file
deadWorker := 0
sent := true
var req *dep_msg.Request
// Loop until all lines are read and a corresponding request has been spooled
Outerloop:
for {
opened := true
// This loop does not only loop when a new request is spooled, but also when a response is received. Thus,
// we need this "sent" switch to know whether we should continue to try to push a request or read a new line
if sent {
sent = false
targetDn := ""
for targetDn == "" {
// Read a domain name
targetDn, opened = <-domainNameChan
if targetDn == "" && !opened {
close(wc)
break Outerloop
}
}
// Build the dependency request
req = dep_msg.NewRequest(targetDn, true, false, reqConf.Exceptions)
}
select {
case wc <- req:
if !opened {
close(wc)
break Outerloop
}
sent = true
case res := <-ansChan:
if handleWorkerResponse(res, reqConf) {
deadWorker++
}
}
}
for deadWorker < transdepConf.JobCount {
res := <-ansChan
if handleWorkerResponse(res, reqConf) {
deadWorker++
}
}
close(ansChan)
}
func analyseFromFile(loadFile string, requestConf *tools.RequestConfig, tree *iradix.Tree) {
fd, err := os.Open(loadFile)
if err != nil {
fmt.Println(err)
return
}
bufrd := bufio.NewReader(fd)
targetDn, err := bufrd.ReadString(':')
if err != nil {
fmt.Println(err)
return
}
targetDn = strings.TrimRight(targetDn, ":")
jsonbstr, err := bufrd.ReadBytes('\x00')
if err != nil && err != io.EOF {
fmt.Println(err)
return
}
g := new(graph.RelationshipNode)
err = json.Unmarshal(jsonbstr, g)
if err != nil {
fmt.Println(err)
return
}
ansChan := make(chan *WorkerResult, 1)
analysisDoneChan := make(chan bool, 1)
go performBackgroundAnalysis(targetDn, g, ansChan, analysisDoneChan, requestConf, tree)
<-analysisDoneChan
wr := <-ansChan
handleWorkerResponse(wr, requestConf)
}
func analyseFromDomainList(domChan <-chan string, reqConf *tools.RequestConfig, transdepConf *tools.TransdepConfig, tree *iradix.Tree) {
df := dependency.NewFinder(transdepConf, tree)
defer df.Stop()
analyseDomains(domChan, reqConf, transdepConf, df, tree)
}
func buildDomainListChan(targetDn, fileName string) <-chan string {
domChan := make(chan string)
if len(targetDn) != 0 {
go func() {
domChan <- targetDn
close(domChan)
}()
} else {
go createDomainNameStreamer(fileName, domChan)
}
return domChan
}
func main() {
var targetDn, fileName, loadFile string
var transdepConf tools.TransdepConfig
var requestConf tools.RequestConfig
tmpdir := os.Getenv("TMPDIR")
if tmpdir == "" {
tmpdir = "/tmp"
}
flag.StringVar(&targetDn, "domain", "", "Indicates the domain name to analyze")
flag.StringVar(&fileName, "file", "", "Indicates the file containing domain to analyze, one per line")
flag.StringVar(&loadFile, "load", "", "Indicates the file containing a dependency graph in JSON format")
flag.IntVar(&transdepConf.JobCount, "jobs", 5, "Indicates the maximum number of concurrent workers")
flag.BoolVar(&requestConf.AnalysisCond.All, "all", false, "Indicates that IPv4 are not available")
flag.BoolVar(&requestConf.AnalysisCond.NoV4, "break4", false, "Indicates that IPv4 are not available")
flag.BoolVar(&requestConf.AnalysisCond.NoV6, "break6", false, "Indicates that IPv6 are not available")
flag.BoolVar(&requestConf.AnalysisCond.DNSSEC, "dnssec", false, "Indicates that only DNSSEC-protected domains can break")
flag.BoolVar(&requestConf.OutputFormat.ScriptFriendlyOutput, "script", false, "On error, just write \"-ERROR-\"")
flag.BoolVar(&requestConf.OutputFormat.Graph, "graph", false, "Indicates whether to just print the graph")
flag.BoolVar(&requestConf.OutputFormat.DotOutput, "dot", false, "Indicates whether to just print the graphviz dot file representation")
flag.IntVar(&transdepConf.LRUSizes.DependencyFinder, "dflrusize", 2000, "Indicates the maximum number of concurrent Dependency Finder workers")
flag.IntVar(&transdepConf.LRUSizes.ZoneCutFinder, "zcflrusize", 10000, "Indicates the maximum number of concurrent Zone Cut Finder workers")
flag.IntVar(&transdepConf.LRUSizes.NameResolverFinder, "nrlrusize", 10000, "Indicates the maximum number of concurrent Name Resolver workers")
flag.StringVar(&transdepConf.CacheRootDir, "cachedir", tmpdir, "Specifies the cache directory")
flag.StringVar(&transdepConf.RootHintsFile, "hints", "", "An updated DNS root hint file. If left unspecified, some hardcoded values will be used.")
flag.StringVar(&transdepConf.MaboFile, "mabo", "", "Indicates the name of a file containing the output of the Mabo tool when used with the prefix option")
flag.BoolVar(&requestConf.Exceptions.RFC8020, "rfc8020", false, "If set, a RCODE=3 on a zonecut request will be considered as an ENT.")
flag.BoolVar(&requestConf.Exceptions.AcceptServFailAsNoData, "servfail", false, "Consider a SERVFAIL error as an ENT (for servers that can't answer to anything else than A and AAAA)")
flag.Parse()
if len(targetDn) == 0 && len(fileName) == 0 && len(loadFile) == 0 {
panic("Either domain parameter, load parameter or file parameter must be specified.")
}
if err := requestConf.Check(fileName) ; err != nil {
panic(err.Error())
}
var tree *iradix.Tree
var err error
if len(transdepConf.MaboFile) != 0 {
tree, err = radix.GetASNTree(transdepConf.MaboFile)
if err != nil {
panic(err)
}
}
if len(loadFile) != 0 {
analyseFromFile(loadFile, &requestConf, tree)
} else {
analyseFromDomainList(buildDomainListChan(targetDn, fileName), &requestConf, &transdepConf, tree)
}
}