2018-01-23 20:25:00 +00:00
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 ,
2018-04-07 10:39:23 +00:00
nil ,
2018-01-23 20:25:00 +00:00
}
} 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 )
}
}