From 98b48988d2737c572ad7694089c9e502ac9b73e5 Mon Sep 17 00:00:00 2001 From: tianyang ni Date: Wed, 14 Sep 2022 12:57:23 +0800 Subject: [PATCH] fix: duplicated nodename causing invalid dot output --- Makefile | 3 + cmd/iftree/main.go | 93 +++++++++------- pkg/formatter/graph.go | 208 ++++++++++++++++++++--------------- pkg/formatter/table.go | 14 ++- pkg/formatter/text.go | 8 +- pkg/netutil/netutil_linux.go | 13 ++- pkg/{ => types}/type.go | 2 +- 7 files changed, 195 insertions(+), 146 deletions(-) create mode 100644 Makefile rename pkg/{ => types}/type.go (98%) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..86c9745 --- /dev/null +++ b/Makefile @@ -0,0 +1,3 @@ +build: + CGO_ENABLED=1 go build --ldflags '-extldflags "-static"' -o iftree cmd/iftree/main.go + diff --git a/cmd/iftree/main.go b/cmd/iftree/main.go index 33bf06d..244d613 100644 --- a/cmd/iftree/main.go +++ b/cmd/iftree/main.go @@ -11,14 +11,15 @@ import ( "syscall" "github.com/containerd/nerdctl/pkg/rootlessutil" - log "github.com/sirupsen/logrus" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/spf13/pflag" "github.com/vishvananda/netlink" "github.com/vishvananda/netns" - "github.com/t1anz0ng/iftree/pkg" "github.com/t1anz0ng/iftree/pkg/formatter" "github.com/t1anz0ng/iftree/pkg/netutil" + "github.com/t1anz0ng/iftree/pkg/types" ) var ( @@ -63,6 +64,7 @@ Help Options: version: %s `, version) } + } func helper() error { @@ -75,77 +77,82 @@ func helper() error { } return nil } + func main() { pflag.Parse() if err := helper(); err != nil { - log.Fatal(err) + logrus.Fatal(err) } if rootlessutil.IsRootless() { - log.Error("iftree must be run as root to enter ns") + logrus.Error("iftree must be run as root to enter ns") os.Exit(1) } - log.SetLevel(log.InfoLevel) + logrus.SetLevel(logrus.InfoLevel) if *debug { - log.SetReportCaller(true) - log.SetLevel(log.DebugLevel) + logrus.SetReportCaller(true) + logrus.SetLevel(logrus.DebugLevel) + } else { + logrus.SetLevel(logrus.ErrorLevel) } + + // Lock the OS Thread so we don't accidentally switch namespaces runtime.LockOSThread() defer runtime.UnlockOSThread() netNsMap, err := netutil.NetNsMap() if err != nil { - log.Fatal(err) + logrus.Fatal(err) } if len(netNsMap) == 0 { - log.Warn("no netns found") + logrus.Warn("no netns found") os.Exit(0) } - log.Debugf("net namespace id <-> name map:\n%+v\n", netNsMap) + logrus.Debugf("net namespace id <-> name map:\n%+v\n", netNsMap) ll, err := netlink.LinkList() if err != nil { - log.Fatal(err) + logrus.Fatal(errors.Unwrap(err)) } - log.Debugf("net link list:\n%+v\n", ll) + logrus.Debugf("net link list:\n%+v\n", ll) - bridgeVethM := make(map[string][]pkg.Node) // map bridge <-> veth paris - unBridgedVpairs := []pkg.Node{} + bridgeVethM := make(map[string][]types.Node) // map bridge <-> veth paris + unBridgedVpairs := []types.Node{} bridgeIps := make(map[string]*net.IP) // bridge ip - loS := []pkg.Node{} // loopback + loS := []types.Node{} // loopback origin, err := netns.Get() if err != nil { - log.Fatalf("failed get current netne, %v", err) + logrus.Fatalf("failed get current netne, %v", err) } - // FIXME: use undirected graph insted of array/map to store relations + for _, link := range ll { veth, ok := link.(*netlink.Veth) if !ok { // skip device not enslaved to any bridge - log.Debugf("skip %s, type: %s", link.Attrs().Name, link.Type()) + logrus.Debugf("skip %s, type: %s", link.Attrs().Name, link.Type()) continue } - log.Debugf("found veth device: %s", veth.Name) + logrus.Debugf("veth device: %+v", veth) peerIdx, err := netlink.VethPeerIndex(veth) if err != nil { - log.Fatal(err) + logrus.Fatal(err) } if link.Attrs().MasterIndex == -1 || veth.MasterIndex == 0 { - log.Debugf("%s not has a bridge as master, MasterIndex: %d", veth.Name, link.Attrs().MasterIndex) + logrus.Debugf("%s not has a bridge as master, MasterIndex: %d", veth.Name, link.Attrs().MasterIndex) if veth.PeerName == "" { p, err := netlink.LinkByIndex(peerIdx) if err != nil { - log.Fatal(err) + logrus.Fatal(err) } veth.PeerName = p.Attrs().Name } routes, err := netlink.RouteList(link, 4) if err != nil { - log.Fatal(err) + logrus.Fatal(err) } - node := pkg.Node{ - Type: pkg.VethType, + node := types.Node{ + Type: types.VethType, Veth: veth.Name, Peer: veth.PeerName, PeerId: peerIdx, @@ -161,7 +168,7 @@ func main() { master, err := netlink.LinkByIndex(veth.Attrs().MasterIndex) if err != nil { - log.Fatal(err) + logrus.Fatal(err) } // if master is not bridge @@ -172,10 +179,10 @@ func main() { bridge := master.Attrs().Name v, ok := bridgeVethM[bridge] if !ok { - bridgeVethM[bridge] = []pkg.Node{} + bridgeVethM[bridge] = []types.Node{} } - pair := pkg.Node{ - Type: pkg.VethType, + pair := types.Node{ + Type: types.VethType, Veth: veth.Name, PeerId: peerIdx, NetNsID: veth.NetNsID, @@ -183,7 +190,7 @@ func main() { if peerNetNs, ok := netNsMap[veth.NetNsID]; ok { peerInNs, err := netutil.GetPeerInNs(peerNetNs, origin, peerIdx) if err != nil { - log.Fatal(err) + logrus.Fatal(err) } pair.NetNsName = peerNetNs pair.PeerNameInNetns = peerInNs.Attrs().Name @@ -191,8 +198,8 @@ func main() { lo, err := netutil.GetLoInNs(peerNetNs, origin) if err == nil && lo != nil { - loS = append(loS, pkg.Node{ - Type: pkg.LoType, + loS = append(loS, types.Node{ + Type: types.LoType, NetNsName: peerNetNs, Status: lo.Attrs().OperState.String(), }) @@ -203,10 +210,10 @@ func main() { addrs, err := netlink.AddrList(master, syscall.AF_INET) if err != nil { - log.Fatal(err) + logrus.Fatal(err) } if len(addrs) > 0 { - pair.Master = &pkg.Bridge{ + pair.Master = &types.Bridge{ Name: bridge, IP: &addrs[0].IP, } @@ -214,14 +221,14 @@ func main() { } bridgeVethM[bridge] = append(v, pair) } - log.Debugf("bridgeVethMap: %+v", bridgeVethM) + logrus.Debugf("bridgeVethMap: %+v", bridgeVethM) switch { case *oGraph: buf := bytes.Buffer{} output, err := formatter.GraphInDOT(bridgeVethM, unBridgedVpairs, loS, bridgeIps) if err != nil { - log.Fatal(err) + logrus.Fatal(errors.Unwrap(err)) } fmt.Fprintln(&buf, output) gType := strings.ToLower(*oGraphType) @@ -231,21 +238,25 @@ func main() { _, err = io.Copy(defaultOutput, &buf) case "jpg", "png", "svg": if !pflag.CommandLine.Changed("output") && !pflag.CommandLine.Changed("gtype") { - log.Warn(`default output dst file: "output.png"`) + logrus.Warn(`default output dst file: "output.png"`) + } + if fn, err := formatter.GenImage(buf.Bytes(), oGraphName, gType); err != nil { + os.Remove(fn) + logrus.Fatal(errors.Unwrap(err)) } - err = formatter.GenImage(buf.Bytes(), oGraphName, gType) default: - log.Fatal("invalid graph type") + logrus.Error("unknown image type") + os.Exit(1) } if err != nil { - log.Fatal(err) + logrus.Fatal(err) } return case *oTable: if len(bridgeVethM) > 0 { err := formatter.Table(defaultOutput, bridgeVethM) if err != nil { - log.Fatal(err) + logrus.Fatal(errors.Unwrap(err)) } } if *oNotBridgedVeths && len(unBridgedVpairs) > 0 { diff --git a/pkg/formatter/graph.go b/pkg/formatter/graph.go index 04e13ee..39e115c 100644 --- a/pkg/formatter/graph.go +++ b/pkg/formatter/graph.go @@ -8,36 +8,72 @@ import ( graph "github.com/awalterschulze/gographviz" graphviz "github.com/goccy/go-graphviz" - log "github.com/sirupsen/logrus" + "github.com/pkg/errors" - "github.com/t1anz0ng/iftree/pkg" + "github.com/t1anz0ng/iftree/pkg/types" ) -func GraphInDOT(m map[string][]pkg.Node, vpairs, los []pkg.Node, bm map[string]*net.IP) (string, error) { +const ( + label = "label" +) +var ( + nodeAttr = map[string]string{ + "style": "filled", + } + netNsAttr = map[string]string{ + "style": "filled", + "color": "grey", + "nodesep": "4.0", + "shape": "box", + } + bridgeAttr = map[string]string{ + "nodesep": "4.0", + "shape": "octagon", + "style": "filled", + "fontsize": "16pt", + } + loAttr = map[string]string{ + "shape": "oval", + "style": "filled", + "color": "#f0c674", + } + edgeAttr = map[string]string{ + "color": "black", + } + redEdgeAttr = map[string]string{ + "color": "red", + "fontcolor": "red", + } + inNetNsAttr = map[string]string{ + "shape": "oval", + "style": "filled", + "color": "#f0c674", + } +) + +func GraphInDOT(brVethsM map[string][]types.Node, vpairs, los []types.Node, bridgeIps map[string]*net.IP) (string, error) { + + graphName := "G" root := graph.NewEscape() - if err := root.SetName("G"); err != nil { + if err := root.SetName(graphName); err != nil { return "", err } - root.AddAttr("G", "layout", "fdp") //nolint:errcheck - root.AddAttr("G", "splines", "ortho") //nolint:errcheck - root.AddAttr("G", "ratio", "0.7") //nolint:errcheck + root.AddAttr(graphName, "layout", "fdp") //nolint:errcheck + root.AddAttr(graphName, "splines", "ortho") //nolint:errcheck + root.AddAttr(graphName, "ratio", "0.7") //nolint:errcheck + subGraphM := make(map[string]*graph.SubGraph) - for bridge, v := range m { + for bridge, v := range brVethsM { labels := []string{bridge} - if ip, ok := bm[bridge]; ok { + if ip, ok := bridgeIps[bridge]; ok { labels = append(labels, ip.String()) } - attr := map[string]string{ - "label": strings.Join(labels, "\\n"), - "nodesep": "4.0", - "shape": "octagon", - "style": "filled", - "fontsize": "16pt", - } - if err := root.AddNode("G", bridge, attr); err != nil { - return "", err + attr := generateAttr(bridgeAttr, strings.Join(labels, "\\n")) + + if err := root.AddNode(graphName, bridge, attr); err != nil { + return "", errors.Wrapf(err, "create bridge node %s", bridge) } for i, vp := range v { // group by vp.NetNsName @@ -45,47 +81,36 @@ func GraphInDOT(m map[string][]pkg.Node, vpairs, los []pkg.Node, bm map[string]* sub, ok := subGraphM[vp.NetNsName] if !ok { // init subgraph for netns - sub = graph.NewSubGraph(fmt.Sprintf("cluster%s%c", bridge, 'A'+i)) + sub = graph.NewSubGraph(fmt.Sprintf("cluster-%s%c", bridge, 'A'+i)) subGraphM[vp.NetNsName] = sub - attr := map[string]string{ - "label": fmt.Sprintf("NetNS\n%s", vp.NetNsName), - "style": "filled", - "color": "grey", - "nodesep": "4.0", - "shape": "box", - } - if err := root.AddSubGraph("G", sub.Name, attr); err != nil { - return "", err + err := root.AddSubGraph(graphName, sub.Name, generateAttr(netNsAttr, fmt.Sprintf("NetNS\n%s", vp.NetNsName))) + if err != nil { + return "", errors.Wrapf(err, "create sub graph [%s] from %+v", sub, vp) } } - if err := root.AddNode("G", vp.Veth, map[string]string{ - "label": vp.Label(), - "style": "filled", - }); err != nil { - return "", err + // host veth + err := root.AddNode(graphName, vp.Veth, generateAttr(nodeAttr, vp.Label())) + if err != nil { + return "", errors.Wrapf(err, "create veth node [%s]", vp.Veth) } - if err := root.AddEdge(vp.Veth, bridge, false, map[string]string{ - "color": "black", - }); err != nil { - return "", err + // bridege <-> veth edge + err = root.AddEdge(vp.Veth, bridge, false, edgeAttr) + if err != nil { + return "", errors.Wrapf(err, "create edge between [%s] and [%s]", vp.Veth, bridge) } - vethInNsName := fmt.Sprintf("%s_%d", vp.PeerNameInNetns, i) - if err := root.AddNode(sub.Name, vethInNsName, map[string]string{ - "label": vp.PeerNameInNetns, - "shape": "oval", - "style": "filled", - "color": "#f0c674", - }); err != nil { - return "", err + + // netns veth + vethInNsName := fmt.Sprintf("%s_%d", bridge, i) + err = root.AddNode(sub.Name, vethInNsName, generateAttr(inNetNsAttr, vp.PeerNameInNetns)) + if err != nil { + return "", errors.Wrapf(err, "create veth node [%s]", vethInNsName) } - if err := root.AddEdge(vp.Veth, vethInNsName, false, map[string]string{ - "color": "red", - "fontcolor": "red", - }); err != nil { - return "", err + // veths edge + err = root.AddEdge(vp.Veth, vethInNsName, false, redEdgeAttr) + if err != nil { + return "", errors.Wrapf(err, "create edge between [%s] and [%s]", vp.Veth, vethInNsName) } - } else { attr := map[string]string{ "label": vp.Veth, @@ -93,31 +118,25 @@ func GraphInDOT(m map[string][]pkg.Node, vpairs, los []pkg.Node, bm map[string]* if vp.Orphaned { attr["label"] += "\n(orphaned)" } - if err := root.AddNode("G", vp.Veth, attr); err != nil { - return "", err + if err := root.AddNode(graphName, vp.Veth, attr); err != nil { + return "", errors.Wrapf(err, "create veth node [%s]", vp.Veth) } - if err := root.AddEdge(vp.Veth, bridge, false, map[string]string{ - "color": "black", - }); err != nil { - return "", err + if err := root.AddEdge(vp.Veth, bridge, false, edgeAttr); err != nil { + return "", errors.Wrapf(err, "create edge between [%s] and [%s]", vp.Veth, bridge) } } } } + + // loopbacks for _, lo := range los { if lo.Status == "" { continue } if sub, ok := subGraphM[lo.NetNsName]; ok { - if err := root.AddNode(sub.Name, - fmt.Sprintf("%s-lo", sub.Name), - map[string]string{ - "label": lo.Label(), - "shape": "oval", - "style": "filled", - "color": "#f0c674", - }); err != nil { - return "", err + err := root.AddNode(sub.Name, fmt.Sprintf("%s-lo", sub.Name), generateAttr(loAttr, lo.Label())) + if err != nil { + return "", errors.Wrapf(err, "create loopback in netns [%s]", lo.NetNsName) } } } @@ -125,42 +144,33 @@ func GraphInDOT(m map[string][]pkg.Node, vpairs, los []pkg.Node, bm map[string]* visited := make(map[string]struct{}) for _, vp := range vpairs { if _, ok := visited[vp.Veth]; !ok { - root.AddNode("G", vp.Veth, //nolint:errcheck - map[string]string{ - "label": vp.Veth, - "style": "filled", - }) + root.AddNode(graphName, vp.Veth, generateAttr(nodeAttr, vp.Veth)) //nolint:errcheck visited[vp.Veth] = struct{}{} } if _, ok := visited[vp.Peer]; !ok { - root.AddNode("G", vp.Peer, //nolint:errcheck - map[string]string{ - "label": vp.Peer, - "style": "filled", - }) + root.AddNode(graphName, vp.Peer, generateAttr(nodeAttr, vp.Peer)) //nolint:errcheck visited[vp.Peer] = struct{}{} } - root.AddEdge(vp.Veth, vp.Peer, false, //nolint:errcheck - map[string]string{ - "color": "black", - }) + root.AddEdge(vp.Veth, vp.Peer, false, generateAttr(edgeAttr, "")) //nolint:errcheck } - return root.String(), nil } -func GenImage(data []byte, oGraphName *string, gType string) (err error) { - graph, errG := graphviz.ParseBytes(data) - if errG != nil { - log.Fatal(errG) - } +func GenImage(data []byte, oGraphName *string, gType string) (fn string, err error) { + g := graphviz.New() - fn := fmt.Sprintf("%s.%s", *oGraphName, gType) - f, errF := os.Create(fn) - if errF != nil { - log.Fatal(errF) + + graph, err := graphviz.ParseBytes(data) + if err != nil { + return "", errors.Wrap(err, "parse dot bytes") + } + fn = fmt.Sprintf("%s.%s", *oGraphName, gType) + f, err := os.Create(fn) + if err != nil { + return fn, errors.Wrapf(err, "create file `%s`", fn) } defer f.Close() + switch gType { case "jpg": err = g.Render(graph, graphviz.JPG, f) @@ -168,6 +178,22 @@ func GenImage(data []byte, oGraphName *string, gType string) (err error) { err = g.Render(graph, graphviz.PNG, f) case "svg": err = g.Render(graph, graphviz.SVG, f) + default: + return fn, fmt.Errorf("unknown graph type %s", gType) + } + if err != nil { + return fn, errors.Wrap(err, "render image") + } + return fn, nil +} + +func generateAttr(base map[string]string, lb string) map[string]string { + m := make(map[string]string) + for k, v := range base { + m[k] = v + } + if lb != "" { + m[label] = lb } - return + return m } diff --git a/pkg/formatter/table.go b/pkg/formatter/table.go index 9512bfa..57a3721 100644 --- a/pkg/formatter/table.go +++ b/pkg/formatter/table.go @@ -9,16 +9,18 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/jedib0t/go-pretty/v6/table" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" - "github.com/t1anz0ng/iftree/pkg" "github.com/t1anz0ng/iftree/pkg/netutil" + "github.com/t1anz0ng/iftree/pkg/types" ) var ( tableNsColors = colorGrid(1, 5) ) -func Table(w io.Writer, m map[string][]pkg.Node) error { +func Table(w io.Writer, m map[string][]types.Node) error { tbStr := strings.Builder{} t := table.NewWriter() @@ -29,9 +31,13 @@ func Table(w io.Writer, m map[string][]pkg.Node) error { for bridge, v := range m { for _, vp := range v { + if vp.NetNsName == "" { + logrus.Warnf("no netns name for %+v", vp) + continue + } id, err := netutil.NsidFromPath(vp.NetNsName) if err != nil { - return err + return errors.Wrapf(err, "get nsid from path `%s`", vp.NetNsName) } c := lipgloss.Color(tableNsColors[id%len(tableNsColors)][0]) t.AppendRow(table.Row{ @@ -54,7 +60,7 @@ func Table(w io.Writer, m map[string][]pkg.Node) error { return nil } -func TableParis(w io.Writer, vpairs []pkg.Node) { +func TableParis(w io.Writer, vpairs []types.Node) { if len(vpairs) == 0 { return diff --git a/pkg/formatter/text.go b/pkg/formatter/text.go index cd4a3f5..e46fbd5 100644 --- a/pkg/formatter/text.go +++ b/pkg/formatter/text.go @@ -7,13 +7,13 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/jedib0t/go-pretty/v6/list" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" - "github.com/t1anz0ng/iftree/pkg" + "github.com/t1anz0ng/iftree/pkg/types" ) -func Print(w io.Writer, vm map[string][]pkg.Node, netNsMap map[int]string, vpairs []pkg.Node, all bool) { +func Print(w io.Writer, vm map[string][]types.Node, netNsMap map[int]string, vpairs []types.Node, all bool) { var contents []string @@ -26,7 +26,7 @@ func Print(w io.Writer, vm map[string][]pkg.Node, netNsMap map[int]string, vpair for k, v := range vm { master, err := netlink.LinkByName(k) if err != nil { - log.Fatal(err) + logrus.Fatal(err) } lw.AppendItem( bridgeStyle.SetString( diff --git a/pkg/netutil/netutil_linux.go b/pkg/netutil/netutil_linux.go index 08bea30..c3474c8 100644 --- a/pkg/netutil/netutil_linux.go +++ b/pkg/netutil/netutil_linux.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/pkg/errors" + "github.com/sirupsen/logrus" "github.com/vishvananda/netlink" "github.com/vishvananda/netns" ) @@ -21,22 +22,24 @@ const ( func NetNsMap() (map[int]string, error) { nsArr, err := listNetNsPath() if err != nil { - return nil, errors.Wrap(err, "failed list netns") + return nil, errors.Wrap(err, "list netns") } - + logrus.Debugf("netns paths: %+v", nsArr) m := make(map[int]string) for _, path := range nsArr { id, err := NsidFromPath(path) if err != nil { - return nil, err + return nil, errors.Wrapf(err, "get nsid from path `%s`", path) } // -1 if the namespace does not have an ID set. - if id != -1 { - m[id] = path + if id == -1 { + continue } + m[id] = path } return m, nil } + func NsidFromPath(path string) (int, error) { netnsFd, err := netns.GetFromPath(path) if err != nil { diff --git a/pkg/type.go b/pkg/types/type.go similarity index 98% rename from pkg/type.go rename to pkg/types/type.go index 8df7c88..ce777a9 100644 --- a/pkg/type.go +++ b/pkg/types/type.go @@ -1,4 +1,4 @@ -package pkg +package types import ( "net"