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) } }