From 7554a8d782db2307d615ab494a6a26f18b03a171 Mon Sep 17 00:00:00 2001 From: Andrea Barberio Date: Fri, 10 Mar 2023 23:58:22 +0100 Subject: [PATCH] [Go] Added Results.ToDOT, and cmd/todot utility Signed-off-by: Andrea Barberio --- go.mod | 4 + go.sum | 19 ++++ go/dublintraceroute/cmd/todot/main.go | 61 ++++++++++++ go/dublintraceroute/results/results.go | 124 +++++++++++++++++++++++++ 4 files changed, 208 insertions(+) create mode 100644 go/dublintraceroute/cmd/todot/main.go diff --git a/go.mod b/go.mod index bd80a3c..d337295 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/florianl/go-nfqueue v1.3.1 + github.com/goccy/go-graphviz v0.1.0 github.com/sirupsen/logrus v1.9.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.2 @@ -13,12 +14,15 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fogleman/gg v1.3.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/google/go-cmp v0.5.7 // indirect github.com/josharian/native v1.0.0 // indirect github.com/mdlayher/netlink v1.6.0 // indirect github.com/mdlayher/socket v0.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/image v0.0.0-20200119044424-58c23975cae1 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e6cbf23..001fc03 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,28 @@ +github.com/corona10/goimagehash v1.0.2 h1:pUfB0LnsJASMPGEZLj7tGY251vF+qLGqOgEP4rUs6kA= +github.com/corona10/goimagehash v1.0.2/go.mod h1:/l9umBhvcHQXVtQO1V6Gp1yD20STawkhRnnX0D1bvVI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/florianl/go-nfqueue v1.3.1 h1:khQ9fYCrjbu5CF8dZF55G2RTIEIQRI0Aj5k3msJR6Gw= github.com/florianl/go-nfqueue v1.3.1/go.mod h1:aHWbgkhryJxF5XxYvJ3oRZpdD4JP74Zu/hP1zuhja+M= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/goccy/go-graphviz v0.1.0 h1:6OqQoQ5PeAiHYe/YcusyeulqBrOkUb16HQ4ctRdyVUU= +github.com/goccy/go-graphviz v0.1.0/go.mod h1:wXVsXxmyMQU6TN3zGRttjNn3h+iCAS7xQFC6TlNvLhk= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0= github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI= github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= +github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 h1:BvoENQQU+fZ9uukda/RzCAL/191HHwJA5b13R6diVlY= +github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -28,12 +39,19 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1 h1:5h3ngYt7+vXCDZCup/HkCQgW5XwmSvR/nA2JmJ0RErg= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -45,6 +63,7 @@ golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/go/dublintraceroute/cmd/todot/main.go b/go/dublintraceroute/cmd/todot/main.go new file mode 100644 index 0000000..2c92238 --- /dev/null +++ b/go/dublintraceroute/cmd/todot/main.go @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ + +package main + +import ( + "encoding/json" + "fmt" + "go/build" + "log" + "os" + + flag "github.com/spf13/pflag" + + "github.com/insomniacslk/dublin-traceroute/go/dublintraceroute/results" +) + +func init() { + // Ensure that CGO is disabled + var ctx build.Context + if ctx.CgoEnabled { + fmt.Println("Disabling CGo") + ctx.CgoEnabled = false + } +} + +var ( + flagOutputFile = flag.StringP("output", "o", "-", "Output file. Use \"-\" to print to standard output") +) + +func main() { + + flag.Parse() + + if len(flag.Args()) != 1 { + log.Fatal("Missing JSON file name") + } + + buf, err := os.ReadFile(flag.Arg(0)) + if err != nil { + log.Fatalf("Failed to read file '%s': %v", flag.Arg(0), err) + } + + var result results.Results + if err := json.Unmarshal(buf, &result); err != nil { + log.Fatalf("Failed to unmarshal JSON into Results: %v", err) + } + output, err := result.ToDOT() + if err != nil { + log.Fatalf("Failed to convert to DOT: %v", err) + } + if *flagOutputFile == "-" { + fmt.Println(output) + } else { + err := os.WriteFile(*flagOutputFile, []byte(output), 0644) + if err != nil { + log.Fatalf("Failed to write DOT file: %v", err) + } + log.Printf("Saved DOT file to %s", *flagOutputFile) + log.Printf("Run `dot -Tpng \"%s\" -o \"%s.png\"` to convert to PNG", *flagOutputFile, *flagOutputFile) + } +} diff --git a/go/dublintraceroute/results/results.go b/go/dublintraceroute/results/results.go index 7b0bd8e..23b2854 100644 --- a/go/dublintraceroute/results/results.go +++ b/go/dublintraceroute/results/results.go @@ -3,12 +3,19 @@ package results import ( + "bytes" "encoding/json" "fmt" + "log" + "math/rand" "net" + "sort" "strconv" "strings" "time" + + "github.com/goccy/go-graphviz" + "github.com/goccy/go-graphviz/cgraph" ) // IP represents some information from the IP header. @@ -160,3 +167,120 @@ func (r *Results) ToJSON(compress bool, indent string) string { } return string(b) } + +// ToDOT encodes a Results object to a DOT file suitable for GraphViz +func (r *Results) ToDOT() (string, error) { + type node struct { + node *cgraph.Node + probe *Probe + } + gv := graphviz.New() + graph, err := gv.Graph() + if err != nil { + return "", fmt.Errorf("failed to create graph: %w", err) + } + graph.SetRankDir(cgraph.BTRank) + + flowIDs := make([]int, 0, len(r.Flows)) + for flowID := range r.Flows { + flowIDs = append(flowIDs, int(flowID)) + } + sort.Ints(flowIDs) + + for _, flowID := range flowIDs { + hops := r.Flows[uint16(flowID)] + if len(hops) == 0 { + log.Printf("No hops for flow ID %d", flowID) + continue + } + var nodes []node + // add first hop + firstNodeName := hops[0].Sent.IP.SrcIP.String() + firstHop, err := graph.CreateNode(firstNodeName) + if err != nil { + return "", fmt.Errorf("failed to create first node: %w", err) + } + firstHop.SetShape(cgraph.RectShape) + nodes = append(nodes, node{node: firstHop, probe: &hops[0]}) + + // then add all the other hops + for idx, hop := range hops { + hop := hop + nodename := fmt.Sprintf("NULL - %d", idx) + label := "*" + hostname := "" + if hop.Received != nil { + nodename = hop.Received.IP.SrcIP.String() + if hop.Name != nodename { + hostname = "\n" + hop.Name + } + // MPLS labels + mpls := "" + if len(hop.Received.ICMP.MPLSLabels) > 0 { + mpls = "MPLS labels: \n" + for _, mplsLabel := range hop.Received.ICMP.MPLSLabels { + mpls += fmt.Sprintf(" - %d, ttl: %d\n", mplsLabel.Label, mplsLabel.TTL) + } + } + label = fmt.Sprintf("%s%s\n%s\n%s", nodename, hostname, hop.Received.ICMP.Description, mpls) + } + n, err := graph.CreateNode(nodename) + if err != nil { + return "", fmt.Errorf("failed to create node '%s': %w", nodename, err) + } + if hop.IsLast { + n.SetShape(cgraph.RectShape) + } + n.SetLabel(label) + nodes = append(nodes, node{node: n, probe: &hop}) + + if hop.IsLast { + break + } + } + // add edges + if len(nodes) <= 1 { + // no edges to add if there is only one node + continue + } + color := rand.Intn(0xffffff) + // start at node 1. Each node back-references the previous one + for idx := 1; idx < len(nodes); idx++ { + if idx >= len(nodes) { + // we are at the second-to-last node + break + } + prev := nodes[idx-1] + cur := nodes[idx] + edgeName := fmt.Sprintf("%s - %s - %d - %d", prev.node.Name(), cur.node.Name(), idx, flowID) + edgeLabel := "" + if idx == 1 { + edgeLabel += fmt.Sprintf( + "srcport %d\ndstport %d", + cur.probe.Sent.UDP.SrcPort, + cur.probe.Sent.UDP.DstPort, + ) + } + if prev.probe.NATID != cur.probe.NATID { + edgeLabel += "\nNAT detected" + } + edgeLabel += fmt.Sprintf("\n%d.%d ms", int(cur.probe.RttUsec/1000), int(cur.probe.RttUsec%1000)) + + edge, err := graph.CreateEdge(edgeName, prev.node, cur.node) + if err != nil { + return "", fmt.Errorf("failed to create edge '%s': %w", edgeName, err) + } + edge.SetLabel(edgeLabel) + edge.SetColor(fmt.Sprintf("#%06x", color)) + } + } + var buf bytes.Buffer + if err := gv.Render(graph, "dot", &buf); err != nil { + return "", fmt.Errorf("failed to render graph: %w", err) + } + if err := graph.Close(); err != nil { + return "", fmt.Errorf("failed to close graph: %w", err) + } + gv.Close() + return buf.String(), nil +}