From 1b4ccc2a304625effeeb13904a4f54bdc788e216 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:17:40 +0200 Subject: [PATCH 01/11] feat: Introduce a NamespaceNode in addition to the BaseNode Signed-off-by: Steffen Vogel --- pkg/base_node.go | 332 +--------------------- pkg/capture.go | 2 +- pkg/cmd.go | 4 +- pkg/debug.go | 2 +- pkg/host.go | 23 +- pkg/interface.go | 4 +- pkg/namespace.go | 10 +- pkg/nat.go | 2 +- pkg/network.go | 12 +- pkg/node.go | 11 +- pkg/ns_node.go | 345 +++++++++++++++++++++++ pkg/{base_node_run.go => ns_node_run.go} | 10 +- pkg/options/common.go | 2 +- pkg/options/{base_node.go => ns_node.go} | 6 +- pkg/run_test.go | 4 +- pkg/switch.go | 29 +- pkg/trace.go | 2 +- 17 files changed, 428 insertions(+), 372 deletions(-) create mode 100644 pkg/ns_node.go rename pkg/{base_node_run.go => ns_node_run.go} (79%) rename pkg/options/{base_node.go => ns_node.go} (83%) diff --git a/pkg/base_node.go b/pkg/base_node.go index 3e63cb6..2b795f4 100644 --- a/pkg/base_node.go +++ b/pkg/base_node.go @@ -4,19 +4,11 @@ package gont import ( - "errors" "fmt" - "net" "os" "path/filepath" - "syscall" - "cunicu.li/gont/v2/internal/utils" - nft "github.com/google/nftables" - nl "github.com/vishvananda/netlink" - "github.com/vishvananda/netns" "go.uber.org/zap" - "golang.org/x/sys/unix" ) type BaseNodeOption interface { @@ -24,147 +16,45 @@ type BaseNodeOption interface { } type BaseNode struct { - *Namespace - - isHostNode bool - network *Network - name string - - BasePath string + network *Network + name string Interfaces []*Interface + BasePath string // Options - ConfiguredInterfaces []*Interface - Tracer *Tracer - Debugger *Debugger - ExistingNamespace string - ExistingDockerContainer string - RedirectToLog bool - EmptyDirs []string - Captures []*Capture + ConfiguredInterfaces []*Interface logger *zap.Logger } -func (n *Network) AddNode(name string, opts ...Option) (*BaseNode, error) { - var err error - - basePath := filepath.Join(n.VarPath, "nodes", name) - for _, path := range []string{"ns", "files"} { - path = filepath.Join(basePath, path) - if err := os.MkdirAll(path, 0o755); err != nil { - return nil, err - } - } - +func (n *Network) addBaseNode(name string, opts ...Option) (*BaseNode, error) { node := &BaseNode{ - name: name, - network: n, - BasePath: basePath, - logger: zap.L().Named("node").With(zap.String("node", name)), + name: name, + network: n, + logger: zap.L().Named("node").With(zap.String("node", name)), } - node.logger.Info("Adding new node") - + // Apply host options for _, opt := range opts { - if nOpt, ok := opt.(BaseNodeOption); ok { - nOpt.ApplyBaseNode(node) + if bnOpt, ok := opt.(BaseNodeOption); ok { + bnOpt.ApplyBaseNode(node) } } - // Create mount point directories - for _, ed := range node.EmptyDirs { - path := filepath.Join(basePath, "files", ed) - + node.BasePath = filepath.Join(n.VarPath, "nodes", name) + for _, path := range []string{"ns", "files"} { + path = filepath.Join(node.BasePath, path) if err := os.MkdirAll(path, 0o755); err != nil { - return nil, fmt.Errorf("failed to create directory: %w", err) - } - - // Create non-existing empty dirs - // TODO: Should we cleanup in Close()? - if _, err := os.Stat(ed); err != nil && errors.Is(err, os.ErrNotExist) { - if err := os.MkdirAll(ed, 0o755); err != nil { - return nil, fmt.Errorf("failed to create directory '%s': %w", ed, err) - } - } - - // Directories containing a hidden .mount file will be bind mounted - // as a whole rather than just the files it contains. - hfn := filepath.Join(path, ".mount") - if err := utils.Touch(hfn); err != nil { - return nil, fmt.Errorf("failed to create file: %w", err) - } - } - - switch { - case node.ExistingNamespace != "": - // Use an existing namespace created by "ip netns add" - nsh, err := netns.GetFromName(node.ExistingNamespace) - if err != nil { - return nil, fmt.Errorf("failed to find existing network namespace %s: %w", node.ExistingNamespace, err) - } - - node.Namespace = &Namespace{ - Name: node.ExistingNamespace, - NsHandle: nsh, - } - - case node.ExistingDockerContainer != "": - // Use an existing net namespace from a Docker container - nsh, err := netns.GetFromDocker(node.ExistingDockerContainer) - if err != nil { - return nil, fmt.Errorf("failed to find existing docker container %s: %w", node.ExistingNamespace, err) - } - - node.Namespace = &Namespace{ - Name: node.ExistingDockerContainer, - NsHandle: nsh, - } - - default: - // Create a new network namespace - nsName := fmt.Sprintf("%s%s-%s", n.NSPrefix, n.Name, name) - if node.Namespace, err = NewNamespace(nsName); err != nil { return nil, err } } - if node.nlHandle == nil { - node.nlHandle, err = nl.NewHandleAt(node.NsHandle) - if err != nil { - return nil, err - } - } - - src := fmt.Sprintf("/proc/self/fd/%d", int(node.NsHandle)) - dst := filepath.Join(basePath, "ns", "net") - if err := utils.Touch(dst); err != nil { - return nil, err - } - if err := unix.Mount(src, dst, "", syscall.MS_BIND, ""); err != nil { - return nil, fmt.Errorf("failed to bind mount netns fd: %s", err) - } - - n.Register(node) + node.logger.Info("Adding new node") return node, nil } -// Getter - -func (n *BaseNode) NetNSHandle() netns.NsHandle { - return n.NsHandle -} - -func (n *BaseNode) NetlinkHandle() *nl.Handle { - return n.nlHandle -} - -func (n *BaseNode) NftConn() *nft.Conn { - return n.nftConn -} - func (n *BaseNode) Name() string { return n.name } @@ -187,195 +77,3 @@ func (n *BaseNode) Interface(name string) *Interface { return nil } - -func (n *BaseNode) ConfigureInterface(i *Interface) error { - logger := n.logger.With(zap.Any("intf", i)) - logger.Info("Configuring interface") - - // Set MTU - if i.LinkAttrs.MTU != 0 { - logger.Info("Setting interface MTU", - zap.Int("mtu", i.LinkAttrs.MTU), - ) - if err := n.nlHandle.LinkSetMTU(i.Link, i.LinkAttrs.MTU); err != nil { - return err - } - } - - // Set L2 (MAC) address - if i.LinkAttrs.HardwareAddr != nil { - logger.Info("Setting interface MAC address", - zap.Any("mac", i.LinkAttrs.HardwareAddr), - ) - if err := n.nlHandle.LinkSetHardwareAddr(i.Link, i.LinkAttrs.HardwareAddr); err != nil { - return err - } - } - - // Set transmit queue length - if i.LinkAttrs.TxQLen > 0 { - logger.Info("Setting interface transmit queue length", - zap.Int("txqlen", i.LinkAttrs.TxQLen), - ) - if err := n.nlHandle.LinkSetTxQLen(i.Link, i.LinkAttrs.TxQLen); err != nil { - return err - } - } - - // Set interface group - if i.LinkAttrs.Group != 0 { - logger.Info("Setting interface group", - zap.Uint32("group", i.LinkAttrs.Group), - ) - if err := n.nlHandle.LinkSetGroup(i.Link, int(i.LinkAttrs.Group)); err != nil { - return err - } - } - - // Setup netem Qdisc - var pHandle uint32 = nl.HANDLE_ROOT - if i.Flags&WithQdiscNetem != 0 { - attr := nl.QdiscAttrs{ - LinkIndex: i.Link.Attrs().Index, - Handle: nl.MakeHandle(1, 0), - Parent: pHandle, - } - - netem := nl.NewNetem(attr, i.Netem) - - logger.Info("Adding Netem qdisc to interface") - if err := n.nlHandle.QdiscAdd(netem); err != nil { - return err - } - - pHandle = netem.Handle - } - - // Setup tbf Qdisc - if i.Flags&WithQdiscTbf != 0 { - i.Tbf.LinkIndex = i.Link.Attrs().Index - i.Tbf.Limit = 0x7000 - i.Tbf.Minburst = 1600 - i.Tbf.Buffer = 300000 - i.Tbf.Peakrate = 0x1000000 - i.Tbf.QdiscAttrs = nl.QdiscAttrs{ - LinkIndex: i.Link.Attrs().Index, - Handle: nl.MakeHandle(2, 0), - Parent: pHandle, - } - - logger.Info("Adding TBF qdisc to interface") - if err := n.nlHandle.QdiscAdd(&i.Tbf); err != nil { - return err - } - } - - // Setting link up - if err := n.nlHandle.LinkSetUp(i.Link); err != nil { - return err - } - - // Start packet capturing if requested on network or host level - captures := []*Capture{} - captures = append(captures, n.network.Captures...) - captures = append(captures, n.Captures...) - captures = append(captures, i.Captures...) - - for _, c := range captures { - if c != nil && (c.FilterInterface == nil || c.FilterInterface(i)) { - if _, err := c.startInterface(i); err != nil { - return fmt.Errorf("failed to capture interface: %w", err) - } - } - } - - n.Interfaces = append(n.Interfaces, i) - - if err := n.network.GenerateHostsFile(); err != nil { - return fmt.Errorf("failed to update hosts file") - } - - return nil -} - -func (n *BaseNode) Close() error { - for _, i := range n.Interfaces { - if err := i.Close(); err != nil { - return err - } - } - - return nil -} - -func (n *BaseNode) Teardown() error { - if err := n.Namespace.Close(); err != nil { - return err - } - - nsMount := filepath.Join(n.BasePath, "ns", "net") - if err := unix.Unmount(nsMount, 0); err != nil { - return err - } - - return os.RemoveAll(n.BasePath) -} - -// WriteProcFS write a value to a path within the ProcFS by entering the namespace of this node. -func (n *BaseNode) WriteProcFS(path, value string) error { - n.logger.Info("Updating procfs", - zap.String("path", path), - zap.String("value", value), - ) - - return n.RunFunc(func() error { - f, err := os.OpenFile(path, os.O_RDWR, 0) - if err != nil { - return err - } - defer f.Close() - - _, err = f.WriteString(value) - - return err - }) -} - -// EnableForwarding enables forwarding for both IPv4 and IPv6 protocols in the kernel for all interfaces -func (n *BaseNode) EnableForwarding() error { - if err := n.WriteProcFS("/proc/sys/net/ipv4/conf/all/forwarding", "1"); err != nil { - return err - } - - return n.WriteProcFS("/proc/sys/net/ipv6/conf/all/forwarding", "1") -} - -// AddRoute adds a route to the node. -func (n *BaseNode) AddRoute(r *nl.Route) error { - n.logger.Info("Add route", - zap.Any("dst", r.Dst), - zap.Any("gw", r.Gw), - ) - - return n.nlHandle.RouteAdd(r) -} - -// AddDefaultRoute adds a default route for this node by providing a default gateway. -func (n *BaseNode) AddDefaultRoute(gw net.IP) error { - if gw.To4() != nil { - return n.AddRoute(&nl.Route{ - Dst: &DefaultIPv4Mask, - Gw: gw, - }) - } - - return n.AddRoute(&nl.Route{ - Dst: &DefaultIPv6Mask, - Gw: gw, - }) -} - -// AddInterface adds an interface to the list of configured interfaces -func (n *BaseNode) AddInterface(i *Interface) { - n.ConfiguredInterfaces = append(n.ConfiguredInterfaces, i) -} diff --git a/pkg/capture.go b/pkg/capture.go index 65693b0..ad5e7bd 100644 --- a/pkg/capture.go +++ b/pkg/capture.go @@ -95,7 +95,7 @@ func (c *Capture) ApplyInterface(i *Interface) { i.Captures = append(i.Captures, c) } -func (c *Capture) ApplyBaseNode(n *BaseNode) { +func (c *Capture) ApplyNamespaceNode(n *NamespaceNode) { n.Captures = append(n.Captures, c) } diff --git a/pkg/cmd.go b/pkg/cmd.go index 8352136..4cc4c61 100644 --- a/pkg/cmd.go +++ b/pkg/cmd.go @@ -47,11 +47,11 @@ type Cmd struct { StderrWriters []io.Writer debuggerInstance *debuggerInstance - node *BaseNode + node *NamespaceNode logger *zap.Logger } -func (n *BaseNode) Command(name string, args ...any) *Cmd { +func (n *NamespaceNode) Command(name string, args ...any) *Cmd { c := &Cmd{ node: n, } diff --git a/pkg/debug.go b/pkg/debug.go index 73170b6..fb9b67f 100644 --- a/pkg/debug.go +++ b/pkg/debug.go @@ -40,7 +40,7 @@ func (d *Debugger) ApplyNetwork(n *Network) { n.Debugger = d } -func (d *Debugger) ApplyBaseNode(n *BaseNode) { +func (d *Debugger) ApplyNamespaceNode(n *NamespaceNode) { n.Debugger = d } diff --git a/pkg/host.go b/pkg/host.go index 7b90229..62a7614 100644 --- a/pkg/host.go +++ b/pkg/host.go @@ -9,7 +9,6 @@ import ( "path/filepath" nl "github.com/vishvananda/netlink" - "go.uber.org/zap" ) type HostOption interface { @@ -17,7 +16,7 @@ type HostOption interface { } type Host struct { - *BaseNode + *NamespaceNode Filter *Filter @@ -32,15 +31,15 @@ func (h *Host) ApplyInterface(i *Interface) { } func (n *Network) AddHost(name string, opts ...Option) (*Host, error) { - node, err := n.AddNode(name, opts...) + node, err := n.AddNamespaceNode(name, opts...) if err != nil { - return nil, fmt.Errorf("failed to create node: %s", err) + return nil, fmt.Errorf("failed to create node: %w", err) } host := &Host{ - BaseNode: node, - Routes: []*nl.Route{}, - FilterRules: []*FilterRule{}, + NamespaceNode: node, + Routes: []*nl.Route{}, + FilterRules: []*FilterRule{}, } n.Register(host) @@ -62,7 +61,7 @@ func (n *Network) AddHost(name string, opts ...Option) (*Host, error) { return nil, fmt.Errorf("failed to configure loopback interface: %w", err) } - if err := host.ConfigureLinks(); err != nil { + if err := host.configureLinks(); err != nil { return nil, fmt.Errorf("failed to configure links: %w", err) } @@ -89,9 +88,9 @@ func (n *Network) AddHost(name string, opts ...Option) (*Host, error) { return host, nil } -// ConfigureLinks adds links to other nodes which +// configureLinks adds links to other nodes which // have been configured by functional options -func (h *Host) ConfigureLinks() error { +func (h *Host) configureLinks() error { for _, intf := range h.ConfiguredInterfaces { peerDev := fmt.Sprintf("veth-%s", h.Name()) @@ -112,8 +111,6 @@ func (h *Host) ConfigureLinks() error { } func (h *Host) ConfigureInterface(i *Interface) error { - h.logger.Info("Configuring interface", zap.Any("intf", i)) - // Disable duplicate address detection (DAD) before adding addresses // so we do not end up with tentative addresses and slow test executions if !i.EnableDAD { @@ -130,7 +127,7 @@ func (h *Host) ConfigureInterface(i *Interface) error { } } - return h.BaseNode.ConfigureInterface(i) + return h.NamespaceNode.ConfigureInterface(i) } func (h *Host) Traceroute(o *Host, opts ...any) error { diff --git a/pkg/interface.go b/pkg/interface.go index d9ba419..d9be2b5 100644 --- a/pkg/interface.go +++ b/pkg/interface.go @@ -31,7 +31,7 @@ var loopbackInterface = Interface{ } type InterfaceOption interface { - ApplyInterface(n *Interface) + ApplyInterface(i *Interface) } func (i *Interface) ApplyBaseNode(n *BaseNode) { @@ -40,7 +40,7 @@ func (i *Interface) ApplyBaseNode(n *BaseNode) { type Interface struct { Name string - Node Node + Node NamespacedNode Link nl.Link Flags int diff --git a/pkg/namespace.go b/pkg/namespace.go index 6cabfb6..701936a 100644 --- a/pkg/namespace.go +++ b/pkg/namespace.go @@ -25,18 +25,18 @@ type Namespace struct { Name string - logger *zap.Logger + loggerNs *zap.Logger } func NewNamespace(name string) (*Namespace, error) { var err error ns := &Namespace{ - Name: name, - logger: zap.L().Named("namespace").With(zap.String("ns", name)), + Name: name, + loggerNs: zap.L().Named("namespace").With(zap.String("ns", name)), } - ns.logger.Info("Creating new namespace") + ns.loggerNs.Info("Creating new namespace") // We lock the goroutine to an OS thread for the duration while we open the netlink sockets runtime.LockOSThread() @@ -73,7 +73,7 @@ func (ns *Namespace) Close() error { return err } - ns.logger.Info("Deleted namespace") + ns.loggerNs.Info("Deleted namespace") } return nil diff --git a/pkg/nat.go b/pkg/nat.go index ce6d263..f6eceb2 100644 --- a/pkg/nat.go +++ b/pkg/nat.go @@ -88,7 +88,7 @@ func (n *Network) AddHostNAT(name string, opts ...Option) (*NAT, error) { } } - if err := host.ConfigureLinks(); err != nil { + if err := host.configureLinks(); err != nil { return nil, err } diff --git a/pkg/network.go b/pkg/network.go index 221be72..45b6a38 100644 --- a/pkg/network.go +++ b/pkg/network.go @@ -60,18 +60,20 @@ func HostNode(n *Network) *Host { } return &Host{ - BaseNode: &BaseNode{ - name: "host", + NamespaceNode: &NamespaceNode{ isHostNode: true, + BaseNode: &BaseNode{ + name: "host", + network: n, + logger: zap.L().Named("host"), + }, Namespace: &Namespace{ Name: "base", NsHandle: baseNs, nlHandle: baseHandle, nftConn: &nft.Conn{}, - logger: zap.L().Named("namespace"), + loggerNs: zap.L().Named("namespace"), }, - network: n, - logger: zap.L().Named("host"), }, } } diff --git a/pkg/node.go b/pkg/node.go index 26c5446..a4074ab 100644 --- a/pkg/node.go +++ b/pkg/node.go @@ -17,9 +17,14 @@ type Node interface { String() string Network() *Network Interface(name string) *Interface - NetNSHandle() netns.NsHandle - NetlinkHandle() *nl.Handle - RunFunc(cb Callback) error ConfigureInterface(i *Interface) error } + +type NamespacedNode interface { + Node + + RunFunc(cb Callback) error + NetNSHandle() netns.NsHandle + NetlinkHandle() *nl.Handle +} diff --git a/pkg/ns_node.go b/pkg/ns_node.go new file mode 100644 index 0000000..7dca4df --- /dev/null +++ b/pkg/ns_node.go @@ -0,0 +1,345 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package gont + +import ( + "errors" + "fmt" + "net" + "os" + "path/filepath" + "syscall" + + "cunicu.li/gont/v2/internal/utils" + nft "github.com/google/nftables" + nl "github.com/vishvananda/netlink" + "github.com/vishvananda/netns" + "go.uber.org/zap" + "golang.org/x/sys/unix" +) + +type NamespaceNodeOption interface { + ApplyNamespaceNode(n *NamespaceNode) +} + +type NamespaceNode struct { + *BaseNode + *Namespace + + isHostNode bool + + // Options + Tracer *Tracer + Debugger *Debugger + ExistingNamespace string + ExistingDockerContainer string + RedirectToLog bool + EmptyDirs []string + Captures []*Capture +} + +func (n *Network) AddNamespaceNode(name string, opts ...Option) (*NamespaceNode, error) { + baseNode, err := n.addBaseNode(name, opts...) + if err != nil { + return nil, err + } + + node := &NamespaceNode{ + BaseNode: baseNode, + } + + // Apply namespaced node options + for _, opt := range opts { + if nOpt, ok := opt.(NamespaceNodeOption); ok { + nOpt.ApplyNamespaceNode(node) + } + } + + // Create mount point directories + for _, ed := range node.EmptyDirs { + path := filepath.Join(node.BasePath, "files", ed) + + if err := os.MkdirAll(path, 0o755); err != nil { + return nil, fmt.Errorf("failed to create directory: %w", err) + } + + // Create non-existing empty dirs + // TODO: Should we cleanup in Close()? + if _, err := os.Stat(ed); err != nil && errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(ed, 0o755); err != nil { + return nil, fmt.Errorf("failed to create directory '%s': %w", ed, err) + } + } + + // Directories containing a hidden .mount file will be bind mounted + // as a whole rather than just the files it contains. + hfn := filepath.Join(path, ".mount") + if err := utils.Touch(hfn); err != nil { + return nil, fmt.Errorf("failed to create file: %w", err) + } + } + + switch { + case node.ExistingNamespace != "": + // Use an existing namespace created by "ip netns add" + nsh, err := netns.GetFromName(node.ExistingNamespace) + if err != nil { + return nil, fmt.Errorf("failed to find existing network namespace %s: %w", node.ExistingNamespace, err) + } + + node.Namespace = &Namespace{ + Name: node.ExistingNamespace, + NsHandle: nsh, + } + + case node.ExistingDockerContainer != "": + // Use an existing net namespace from a Docker container + nsh, err := netns.GetFromDocker(node.ExistingDockerContainer) + if err != nil { + return nil, fmt.Errorf("failed to find existing docker container %s: %w", node.ExistingNamespace, err) + } + + node.Namespace = &Namespace{ + Name: node.ExistingDockerContainer, + NsHandle: nsh, + } + + default: + // Create a new network namespace + nsName := fmt.Sprintf("%s%s-%s", n.NSPrefix, n.Name, name) + if node.Namespace, err = NewNamespace(nsName); err != nil { + return nil, err + } + } + + if node.nlHandle == nil { + node.nlHandle, err = nl.NewHandleAt(node.NsHandle) + if err != nil { + return nil, err + } + } + + src := fmt.Sprintf("/proc/self/fd/%d", int(node.NsHandle)) + dst := filepath.Join(node.BasePath, "ns", "net") + if err := utils.Touch(dst); err != nil { + return nil, err + } + if err := unix.Mount(src, dst, "", syscall.MS_BIND, ""); err != nil { + return nil, fmt.Errorf("failed to bind mount netns fd: %s", err) + } + + n.Register(node) + + return node, nil +} + +// Getter + +func (n *NamespaceNode) Name() string { + return n.BaseNode.Name() +} + +func (n *NamespaceNode) NetNSHandle() netns.NsHandle { + return n.NsHandle +} + +func (n *NamespaceNode) NetlinkHandle() *nl.Handle { + return n.nlHandle +} + +func (n *NamespaceNode) NftConn() *nft.Conn { + return n.nftConn +} + +func (n *NamespaceNode) ConfigureInterface(i *Interface) error { + logger := n.logger.With(zap.Any("intf", i)) + logger.Info("Configuring interface") + + // Set MTU + if i.LinkAttrs.MTU != 0 { + logger.Info("Setting interface MTU", + zap.Int("mtu", i.LinkAttrs.MTU), + ) + if err := n.nlHandle.LinkSetMTU(i.Link, i.LinkAttrs.MTU); err != nil { + return err + } + } + + // Set L2 (MAC) address + if i.LinkAttrs.HardwareAddr != nil { + logger.Info("Setting interface MAC address", + zap.Any("mac", i.LinkAttrs.HardwareAddr), + ) + if err := n.nlHandle.LinkSetHardwareAddr(i.Link, i.LinkAttrs.HardwareAddr); err != nil { + return err + } + } + + // Set transmit queue length + if i.LinkAttrs.TxQLen > 0 { + logger.Info("Setting interface transmit queue length", + zap.Int("txqlen", i.LinkAttrs.TxQLen), + ) + if err := n.nlHandle.LinkSetTxQLen(i.Link, i.LinkAttrs.TxQLen); err != nil { + return err + } + } + + // Set interface group + if i.LinkAttrs.Group != 0 { + logger.Info("Setting interface group", + zap.Uint32("group", i.LinkAttrs.Group), + ) + if err := n.nlHandle.LinkSetGroup(i.Link, int(i.LinkAttrs.Group)); err != nil { + return err + } + } + + // Setup netem Qdisc + var pHandle uint32 = nl.HANDLE_ROOT + if i.Flags&WithQdiscNetem != 0 { + attr := nl.QdiscAttrs{ + LinkIndex: i.Link.Attrs().Index, + Handle: nl.MakeHandle(1, 0), + Parent: pHandle, + } + + netem := nl.NewNetem(attr, i.Netem) + + logger.Info("Adding Netem qdisc to interface") + if err := n.nlHandle.QdiscAdd(netem); err != nil { + return err + } + + pHandle = netem.Handle + } + + // Setup tbf Qdisc + if i.Flags&WithQdiscTbf != 0 { + i.Tbf.LinkIndex = i.Link.Attrs().Index + i.Tbf.Limit = 0x7000 + i.Tbf.Minburst = 1600 + i.Tbf.Buffer = 300000 + i.Tbf.Peakrate = 0x1000000 + i.Tbf.QdiscAttrs = nl.QdiscAttrs{ + LinkIndex: i.Link.Attrs().Index, + Handle: nl.MakeHandle(2, 0), + Parent: pHandle, + } + + logger.Info("Adding TBF qdisc to interface") + if err := n.nlHandle.QdiscAdd(&i.Tbf); err != nil { + return err + } + } + + // Setting link up + if err := n.nlHandle.LinkSetUp(i.Link); err != nil { + return err + } + + // Start packet capturing if requested on network or host level + captures := []*Capture{} + captures = append(captures, n.network.Captures...) + captures = append(captures, n.Captures...) + captures = append(captures, i.Captures...) + + for _, c := range captures { + if c != nil && (c.FilterInterface == nil || c.FilterInterface(i)) { + if _, err := c.startInterface(i); err != nil { + return fmt.Errorf("failed to capture interface: %w", err) + } + } + } + + n.Interfaces = append(n.Interfaces, i) + + if err := n.network.GenerateHostsFile(); err != nil { + return fmt.Errorf("failed to update hosts file") + } + + return nil +} + +func (n *NamespaceNode) Close() error { + for _, i := range n.Interfaces { + if err := i.Close(); err != nil { + return err + } + } + + return nil +} + +func (n *NamespaceNode) Teardown() error { + if err := n.Namespace.Close(); err != nil { + return err + } + + nsMount := filepath.Join(n.BasePath, "ns", "net") + if err := unix.Unmount(nsMount, 0); err != nil { + return err + } + + return os.RemoveAll(n.BasePath) +} + +// WriteProcFS write a value to a path within the ProcFS by entering the namespace of this node. +func (n *NamespaceNode) WriteProcFS(path, value string) error { + n.logger.Info("Updating procfs", + zap.String("path", path), + zap.String("value", value), + ) + + return n.RunFunc(func() error { + f, err := os.OpenFile(path, os.O_RDWR, 0) + if err != nil { + return err + } + defer f.Close() + + _, err = f.WriteString(value) + + return err + }) +} + +// EnableForwarding enables forwarding for both IPv4 and IPv6 protocols in the kernel for all interfaces +func (n *NamespaceNode) EnableForwarding() error { + if err := n.WriteProcFS("/proc/sys/net/ipv4/conf/all/forwarding", "1"); err != nil { + return err + } + + return n.WriteProcFS("/proc/sys/net/ipv6/conf/all/forwarding", "1") +} + +// AddRoute adds a route to the node. +func (n *NamespaceNode) AddRoute(r *nl.Route) error { + n.logger.Info("Add route", + zap.Any("dst", r.Dst), + zap.Any("gw", r.Gw), + ) + + return n.nlHandle.RouteAdd(r) +} + +// AddDefaultRoute adds a default route for this node by providing a default gateway. +func (n *NamespaceNode) AddDefaultRoute(gw net.IP) error { + if gw.To4() != nil { + return n.AddRoute(&nl.Route{ + Dst: &DefaultIPv4Mask, + Gw: gw, + }) + } + + return n.AddRoute(&nl.Route{ + Dst: &DefaultIPv6Mask, + Gw: gw, + }) +} + +// AddInterface adds an interface to the list of configured interfaces +func (n *NamespaceNode) AddInterface(i *Interface) { + n.ConfiguredInterfaces = append(n.ConfiguredInterfaces, i) +} diff --git a/pkg/base_node_run.go b/pkg/ns_node_run.go similarity index 79% rename from pkg/base_node_run.go rename to pkg/ns_node_run.go index bf36c36..95e3155 100644 --- a/pkg/base_node_run.go +++ b/pkg/ns_node_run.go @@ -15,17 +15,17 @@ type GoBuildFlagsOption interface { ApplyGoBuildFlags(*GoBuildFlags) } -func (n *BaseNode) Run(cmd string, args ...any) (*Cmd, error) { +func (n *NamespaceNode) Run(cmd string, args ...any) (*Cmd, error) { c := n.Command(cmd, args...) return c, c.Run() } -func (n *BaseNode) Start(cmd string, args ...any) (*Cmd, error) { +func (n *NamespaceNode) Start(cmd string, args ...any) (*Cmd, error) { c := n.Command(cmd, args...) return c, c.Start() } -func (n *BaseNode) StartGo(fileOrPkg string, args ...any) (*Cmd, error) { +func (n *NamespaceNode) StartGo(fileOrPkg string, args ...any) (*Cmd, error) { bin, err := n.BuildGo(fileOrPkg, args...) if err != nil { return nil, fmt.Errorf("failed to build: %w", err) @@ -34,7 +34,7 @@ func (n *BaseNode) StartGo(fileOrPkg string, args ...any) (*Cmd, error) { return n.Start(bin.Name(), args...) } -func (n *BaseNode) RunGo(fileOrPkg string, args ...any) (*Cmd, error) { +func (n *NamespaceNode) RunGo(fileOrPkg string, args ...any) (*Cmd, error) { bin, err := n.BuildGo(fileOrPkg, args...) if err != nil { return nil, fmt.Errorf("failed to build: %w", err) @@ -43,7 +43,7 @@ func (n *BaseNode) RunGo(fileOrPkg string, args ...any) (*Cmd, error) { return n.Run(bin.Name(), args...) } -func (n *BaseNode) BuildGo(fileOrPkg string, args ...any) (*os.File, error) { +func (n *NamespaceNode) BuildGo(fileOrPkg string, args ...any) (*os.File, error) { if err := os.MkdirAll(n.network.TmpPath, 0o644); err != nil { return nil, fmt.Errorf("failed to create temporary directory: %w", err) } diff --git a/pkg/options/common.go b/pkg/options/common.go index dfee01d..3e77c4b 100644 --- a/pkg/options/common.go +++ b/pkg/options/common.go @@ -12,7 +12,7 @@ func (l RedirectToLog) ApplyNetwork(n *gont.Network) { n.RedirectToLog = bool(l) } -func (l RedirectToLog) ApplyBaseNode(n *gont.BaseNode) { +func (l RedirectToLog) ApplyNamespaceNode(n *gont.NamespaceNode) { n.RedirectToLog = bool(l) } diff --git a/pkg/options/base_node.go b/pkg/options/ns_node.go similarity index 83% rename from pkg/options/base_node.go rename to pkg/options/ns_node.go index d8a51c3..1c41c8f 100644 --- a/pkg/options/base_node.go +++ b/pkg/options/ns_node.go @@ -10,21 +10,21 @@ import ( // The name of an existing network namespace which is used instead of creating a new one. type ExistingNamespace string -func (e ExistingNamespace) ApplyBaseNode(n *g.BaseNode) { +func (e ExistingNamespace) ApplyNamespaceNode(n *g.NamespaceNode) { n.ExistingNamespace = string(e) } // Name of an existing Docker container which is used for this node type ExistingDockerContainer string -func (d ExistingDockerContainer) ApplyBaseNode(n *g.BaseNode) { +func (d ExistingDockerContainer) ApplyNamespaceNode(n *g.NamespaceNode) { n.ExistingDockerContainer = string(d) } // Mount an empty dir to shadow parts of the root filesystem type EmptyDir string -func (ed EmptyDir) ApplyBaseNode(n *g.BaseNode) { +func (ed EmptyDir) ApplyNamespaceNode(n *g.NamespaceNode) { n.EmptyDirs = append(n.EmptyDirs, string(ed)) } diff --git a/pkg/run_test.go b/pkg/run_test.go index 455cf3f..bfb3232 100644 --- a/pkg/run_test.go +++ b/pkg/run_test.go @@ -13,11 +13,11 @@ import ( "github.com/vishvananda/netns" ) -func prepare(t *testing.T) (*g.Network, *g.BaseNode) { +func prepare(t *testing.T) (*g.Network, *g.NamespaceNode) { n, err := g.NewNetwork(*nname, globalNetworkOptions...) require.NoError(t, err, "Failed to create new network") - n1, err := n.AddNode("n1") + n1, err := n.AddNamespaceNode("n1") require.NoError(t, err, "Failed to create node") return n, n1 diff --git a/pkg/switch.go b/pkg/switch.go index ea3e9b0..1d31e35 100644 --- a/pkg/switch.go +++ b/pkg/switch.go @@ -20,7 +20,7 @@ type BridgeOption interface { // Switch is an abstraction for a Linux virtual bridge type Switch struct { - *BaseNode + *NamespaceNode } // Options @@ -31,13 +31,13 @@ func (sw *Switch) ApplyInterface(i *Interface) { // AddSwitch adds a new Linux virtual bridge in a dedicated namespace func (n *Network) AddSwitch(name string, opts ...Option) (*Switch, error) { - node, err := n.AddNode(name, opts...) + node, err := n.AddNamespaceNode(name, opts...) if err != nil { return nil, fmt.Errorf("failed to create node: %w", err) } sw := &Switch{ - BaseNode: node, + NamespaceNode: node, } n.Register(sw) @@ -74,10 +74,19 @@ func (n *Network) AddSwitch(name string, opts ...Option) (*Switch, error) { return nil, fmt.Errorf("failed to bring bridge up: %w", err) } - // Connect host to switch interfaces - for _, intf := range sw.Interfaces { - peerDev := fmt.Sprintf("veth-%s", name) + // Configure links + if err := sw.configureLinks(); err != nil { + return nil, err + } + return sw, nil +} + +// configureLinks adds links to other nodes which +// have been configured by functional options +func (sw *Switch) configureLinks() error { + for _, intf := range sw.ConfiguredInterfaces { + peerDev := fmt.Sprintf("veth-%s", sw.Name()) left := intf left.Node = sw @@ -86,12 +95,12 @@ func (n *Network) AddSwitch(name string, opts ...Option) (*Switch, error) { Node: intf.Node, } - if err := n.AddLink(left, right); err != nil { - return nil, fmt.Errorf("failed to add link: %w", err) + if err := sw.network.AddLink(left, right); err != nil { + return err } } - return sw, nil + return nil } // ConfigureInterface attaches an existing interface to a bridge interface @@ -112,5 +121,5 @@ func (sw *Switch) ConfigureInterface(i *Interface) error { return err } - return sw.BaseNode.ConfigureInterface(i) + return sw.NamespaceNode.ConfigureInterface(i) } diff --git a/pkg/trace.go b/pkg/trace.go index 6161d10..92c31c3 100644 --- a/pkg/trace.go +++ b/pkg/trace.go @@ -25,7 +25,7 @@ func (t *Tracer) ApplyNetwork(n *Network) { n.Tracer = t } -func (t *Tracer) ApplyBaseNode(n *BaseNode) { +func (t *Tracer) ApplyNamespaceNode(n *NamespaceNode) { n.Tracer = t } From 04f03d789f5271eae14691da90936336b64b76ac Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:17:52 +0200 Subject: [PATCH 02/11] fix(doc): Update README Signed-off-by: Steffen Vogel --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b87126..d515ab0 100644 --- a/README.md +++ b/README.md @@ -153,9 +153,9 @@ classDiagram Network "1" o-- "*" Node ``` --> -## Authors +## Contact -- Steffen Vogel ([@stv0g](https://github.com/stv0g)) +Please have a look at the contact page: [cunicu.li/docs/contact](https://cunicu.li/docs/contact). ### Funding acknowledment From bd118179daf3b0e37d25406c2e02b21db296b23d Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:18:10 +0200 Subject: [PATCH 03/11] feat: Add new Make target for running tests Signed-off-by: Steffen Vogel --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index d3e373f..e2bbb85 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,9 @@ GOFLAGS = -ldflags "-X main.tag=$(GIT_TAG)" all: gontc +tests: + sudo -E go test ./pkg ./internal -v $(TEST_OPTS) + gontc: go build $(GOFLAGS) -o $@ ./cmd/gontc From ac7b6c8d8387cb9af5c96f52293d20c3a0152a79 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:20:44 +0200 Subject: [PATCH 04/11] fix(nix): Update Flake Signed-off-by: Steffen Vogel --- default.nix | 10 ++++++++ flake.lock | 6 ++--- flake.nix | 73 ++++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 65 insertions(+), 24 deletions(-) create mode 100644 default.nix diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..1dc528b --- /dev/null +++ b/default.nix @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2023 Steffen Vogel +# SPDX-License-Identifier: Apache-2.0 +{ buildGoModule, libpcap }: +buildGoModule { + name = "gont"; + src = ./.; + vendorHash = "sha256-9/TPK8MD1lA9s3jhKmHweY7quw383kHgrcL2XLyuQ54="; + buildInputs = [ libpcap ]; + doCheck = false; +} diff --git a/flake.lock b/flake.lock index 5e9fe1f..90b5b72 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1716137900, - "narHash": "sha256-sowPU+tLQv8GlqtVtsXioTKeaQvlMz/pefcdwg8MvfM=", + "lastModified": 1718318537, + "narHash": "sha256-4Zu0RYRcAY/VWuu6awwq4opuiD//ahpc2aFHg2CWqFY=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6c0b7a92c30122196a761b440ac0d46d3d9954f1", + "rev": "e9ee548d90ff586a6471b4ae80ae9cfcbceb3420", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ace243d..698f612 100644 --- a/flake.nix +++ b/flake.nix @@ -17,31 +17,62 @@ flake-utils.lib.eachDefaultSystem ( system: let - pkgs = nixpkgs.legacyPackages.${system}; - in - rec { - packages.gont = pkgs.buildGoModule { - name = "gont"; - src = ./.; - vendorHash = "sha256-QOh1jBR7FL/fKFmJv7wGxuCghRLR3DV/0TzXd+bUFP0="; - buildInputs = with pkgs; [ libpcap ]; - doCheck = false; + pkgs = import nixpkgs { + inherit system; + inherit overlays; }; - devShell = pkgs.mkShell { - packages = with pkgs; [ - golangci-lint - reuse - traceroute - gnumake - tshark - packages.gont - ]; - - inputsFrom = [ packages.gont ]; + overlay = final: prev: { gont = final.callPackage ./default.nix { }; }; + overlays = [ overlay ]; + + luaPkgs = { + lua-struct = pkgs.lua.pkgs.buildLuarocksPackage { + pname = "lua-struct"; + version = "0.9.2-1"; + + src = pkgs.fetchFromGitHub { + owner = "iryont"; + repo = "lua-struct"; + rev = "0.9.2-1"; + hash = "sha256-tyZU+Cm/f3urG3A5nBFC2NZ9nwtWh1yD4Oj0MHRtDlI="; + }; + }; }; + in + { + inherit overlays; + + packages.default = pkgs.gont; + + devShell = + let + tshark = pkgs.tshark.overrideAttrs ( + final: prev: { + # Make sure we can load our own Lua dissector plugin + # by Wiresharks Lua interpreter when tests are executed by root + postFixup = '' + echo "run_user_scripts_when_superuser = true" >> $out/lib/wireshark/plugins/init.lua + ''; + } + ); + in + pkgs.mkShell { + packages = with pkgs // luaPkgs; [ + golangci-lint + reuse + traceroute + gnumake + tshark + gont + lua-struct + ]; + + inputsFrom = with pkgs; [ gont ]; + + hardeningDisable = [ "fortify" ]; + }; - formatter = nixpkgs.nixfmt-rfc-style; + formatter = pkgs.nixfmt-rfc-style; } ); } From 57e57562f136a6df3264a4c84793a8d7ff90e6cd Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:29:58 +0200 Subject: [PATCH 05/11] fix: Allow enabling WireShark tests Signed-off-by: Steffen Vogel --- pkg/trace_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/trace_test.go b/pkg/trace_test.go index 1d5fca7..705fafc 100644 --- a/pkg/trace_test.go +++ b/pkg/trace_test.go @@ -152,7 +152,7 @@ func TestTraceLog(t *testing.T) { } func TestTraceWithCapture(t *testing.T) { - if _, ok := os.LookupEnv("GITHUB_WORKFLOW"); ok { + if _, ok := os.LookupEnv("WITH_WIRESHARK"); !ok { t.Skip("Requires WireShark") } From dfa3576d27725e4bbc2c8794a6df1d8735cb1f1d Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:30:22 +0200 Subject: [PATCH 06/11] fix: Use same types in test assertions Signed-off-by: Steffen Vogel --- pkg/qdisc_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/qdisc_test.go b/pkg/qdisc_test.go index f047a48..8892bf9 100644 --- a/pkg/qdisc_test.go +++ b/pkg/qdisc_test.go @@ -80,7 +80,7 @@ func TestNetemLoss(t *testing.T) { t.Logf("Loss: %f", stats.PacketLoss) - require.Less(t, math.Abs(stats.PacketLoss-float64(ne.Loss)), 20) + require.Less(t, math.Abs(stats.PacketLoss-float64(ne.Loss)), 20.0) } func TestNetemDuplication(t *testing.T) { @@ -100,5 +100,5 @@ func TestNetemDuplication(t *testing.T) { t.Logf("Duplicate packets: %d", stats.PacketsRecvDuplicates) t.Logf("Duplicate percentage: %.2f %%", duplicatePercentage) - require.Less(t, math.Abs(duplicatePercentage-float64(ne.Duplicate)), 10) + require.Less(t, math.Abs(duplicatePercentage-float64(ne.Duplicate)), 10.0) } From a154bb00bf742724d04812607e3504bac5cbeeac Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:30:46 +0200 Subject: [PATCH 07/11] fix: Replace ICMP-unreachable host by cunicu.li Signed-off-by: Steffen Vogel --- pkg/nat_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/nat_test.go b/pkg/nat_test.go index 8ead4fc..babd1a0 100644 --- a/pkg/nat_test.go +++ b/pkg/nat_test.go @@ -176,7 +176,7 @@ func TestPingHostNAT(t *testing.T) { _, err = h1.Run("ping", "-c", 1, "1.1.1.1") require.NoError(t, err, "Failed to ping") - _, err = h1.Run("ping", "-c", 1, "www.rwth-aachen.de") + _, err = h1.Run("ping", "-c", 1, "cunicu.li") require.NoError(t, err) _, err = h1.Ping(n.HostNode) From 1851f5363f827091110bcfbd8d643ae6801c14db Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:31:20 +0200 Subject: [PATCH 08/11] fix: Add missing call to flags.Parse() Signed-off-by: Steffen Vogel --- pkg/main_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/main_test.go b/pkg/main_test.go index fea32bb..7a9e963 100644 --- a/pkg/main_test.go +++ b/pkg/main_test.go @@ -45,6 +45,8 @@ func TestMain(m *testing.M) { logger := setupLogging() defer logger.Sync() //nolint:errcheck + flag.Parse() + // Handle global flags if *persist { globalNetworkOptions = append(globalNetworkOptions, From fe692a43342b0d0d648e84d8804e8017767f1982 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:31:40 +0200 Subject: [PATCH 09/11] fix: Start NAT device groups with 1000 instead of 1001 Signed-off-by: Steffen Vogel --- pkg/groups.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/groups.go b/pkg/groups.go index c99b440..bac5099 100644 --- a/pkg/groups.go +++ b/pkg/groups.go @@ -7,6 +7,6 @@ type DeviceGroup uint32 const ( DeviceGroupDefault DeviceGroup = 0 - DeviceGroupSouthBound DeviceGroup = 1000 + iota - DeviceGroupNorthBound + DeviceGroupSouthBound DeviceGroup = 1000 + DeviceGroupNorthBound DeviceGroup = 1001 ) From fe76bc18e199076fba6278d59f96b54ba9779ace Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 19:15:53 +0200 Subject: [PATCH 10/11] fix: Linter warnings Signed-off-by: Steffen Vogel --- .golangci.yaml | 131 ++++++++++++++++++++++++++----- cmd/gontc/gontc.go | 22 ++++-- internal/execvpe/execvpe.go | 23 +++--- internal/prque/prque.go | 6 +- internal/utils/ip.go | 10 ++- pkg/capture.go | 12 ++- pkg/capture_interface.go | 6 +- pkg/capture_pcapgo.go | 7 +- pkg/capture_test.go | 33 ++++---- pkg/cmd.go | 18 +++-- pkg/debug_instance.go | 6 +- pkg/debug_instance_breakpoint.go | 2 +- pkg/debug_msg.go | 13 ++- pkg/debug_vscode.go | 14 +++- pkg/exec.go | 1 + pkg/filter_test.go | 2 + pkg/gont.go | 4 +- pkg/host.go | 6 +- pkg/host_ping.go | 11 ++- pkg/link.go | 18 +++-- pkg/main_test.go | 3 +- pkg/nat.go | 7 +- pkg/network.go | 6 +- pkg/network_files.go | 2 +- pkg/ns_node.go | 4 +- pkg/options/bridge.go | 2 +- pkg/options/debug/tracepoint.go | 2 +- pkg/options/filters/network.go | 4 +- pkg/options/options.go | 8 +- pkg/switch.go | 4 +- pkg/trace.go | 16 ++-- pkg/trace/event.go | 2 + pkg/trace/log.go | 2 +- pkg/trace/trace.go | 13 ++- pkg/trace_test.go | 2 +- test/tracee1/main.go | 5 +- 36 files changed, 300 insertions(+), 127 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index db7a50a..bc98353 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,23 +1,118 @@ -# SPDX-FileCopyrightText: 2023 Steffen Vogel +# SPDX-FileCopyrightText: 2023-2024 Steffen Vogel # SPDX-License-Identifier: Apache-2.0 +linters-settings: + misspell: + locale: US + + exhaustive: + default-signifies-exhaustive: true + + gomodguard: + blocked: + modules: + - github.com/pkg/errors: + recommendations: + - errors + + tagliatelle: + case: + use-field-name: true + rules: + json: snake + yaml: snake + xml: snake + + gci: + sections: + - standard + - default + - prefix(cunicu.li/skeleton) + - blank + - dot + + custom-order: true + linters: enable: - - errname - - exportloopref - - gci - - gochecknoglobals - - gocognit - - gofmt - - misspell - - tagliatelle - - whitespace - - revive - - gosec - - nestif - - nolintlint - - prealloc + - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers + - bidichk # Checks for dangerous unicode character sequences + - bodyclose # checks whether HTTP response body is closed successfully + - contextcheck # check the function whether use a non-inherited context + - decorder # check declaration order and count of types, constants, variables and functions + - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) + - dupl # Tool for code clone detection + - durationcheck # check for two durations multiplied together + - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases + - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted. + - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. + - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. + - exhaustive # check exhaustiveness of enum switch statements + - exportloopref # checks for pointers to enclosing loop variables + - forcetypeassert # finds forced type assertions + - gci # Gci control golang package import order and make it always deterministic. + - gochecknoglobals # Checks that no globals are present in Go code + - gochecknoinits # Checks that no init functions are present in Go code + - gocognit # Computes and checks the cognitive complexity of functions + - goconst # Finds repeated strings that could be replaced by a constant + - gocritic # The most opinionated Go source code linter + - err113 # Golang linter to check the errors handling expressions + - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification + - gofumpt # Gofumpt checks whether code was gofumpt-ed. + - goheader # Checks is file header matches to pattern + - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports + - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. + - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. + - goprintffuncname # Checks that printf-like functions are named with `f` at the end + - gosec # Inspects source code for security problems + - gosimple # Linter for Go source code that specializes in simplifying a code + - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string + - grouper # An analyzer to analyze expression groups. + - importas # Enforces consistent import aliases + - ineffassign # Detects when assignments to existing variables are not used + - misspell # Finds commonly misspelled English words in comments + - nakedret # Finds naked returns in functions greater than a specified function length + - nilerr # Finds the code that returns nil even if it checks that the error is not nil. + - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. + - noctx # noctx finds sending http request without context.Context + - predeclared # find code that shadows one of Go's predeclared identifiers + - revive # golint replacement, finds style mistakes + - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks + - stylecheck # Stylecheck is a replacement for golint + - tagliatelle # Checks the struct tags. + - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 + - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes + - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code + - unconvert # Remove unnecessary type conversions + - unparam # Reports unused function parameters + - unused # Checks Go code for unused constants, variables, functions and types + - wastedassign # wastedassign finds wasted assignment statements + - whitespace # Tool for detection of leading and trailing whitespace -linters-settings: - revive: - severity: warning + disable: + - containedctx # containedctx is a linter that detects struct contained context.Context field + - cyclop # checks function and package cyclomatic complexity + - depguard # Go linter that checks if package imports are in a list of acceptable packages + - forbidigo # Forbids identifiers + - funlen # Tool for detection of long functions + - gocyclo # Computes and checks the cyclomatic complexity of functions + - godot # Check if comments end in a period + - godox # Tool for detection of FIXME, TODO and other comment keywords + - gomnd # An analyzer to detect magic numbers. + - ireturn # Accept Interfaces, Return Concrete Types + - lll # Reports long lines + - maintidx # maintidx measures the maintainability index of each function. + - makezero # Finds slice declarations with non-zero initial length + - nestif # Reports deeply nested if statements + - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity + - nolintlint # Reports ill-formed or insufficient nolint directives + - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test + - prealloc # Finds slice declarations that could potentially be preallocated + - promlinter # Check Prometheus metrics naming via promlint + - rowserrcheck # checks whether Err of rows is checked successfully + - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. + - testpackage # linter that makes you use a separate _test package + - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers + - varnamelen # checks that the length of a variable's name matches its scope + - wrapcheck # Checks that errors returned from external packages are wrapped + - wsl # Whitespace Linter - Forces you to use empty lines! diff --git a/cmd/gontc/gontc.go b/cmd/gontc/gontc.go index ed99788..bd2620f 100644 --- a/cmd/gontc/gontc.go +++ b/cmd/gontc/gontc.go @@ -18,6 +18,13 @@ import ( "golang.org/x/exp/slices" ) +var ( + ErrNonExistingNetwork = errors.New("non-existing Gont network") + ErrNonExistingNode = errors.New("non-existing Gont node") + ErrNotEnoughArguments = errors.New("not enough arguments") + ErrUnknownSubCommand = errors.New("unknown sub-command") +) + // Set via ldflags (see Makefile) var tag string //nolint:gochecknoglobals @@ -53,8 +60,7 @@ func main() { var err error var network, node string - logger := internal.SetupLogging() - defer logger.Sync() //nolint:errcheck + internal.SetupLogging() if err := g.CheckCaps(); err != nil { fmt.Printf("error: %s\n", err) @@ -102,7 +108,7 @@ func main() { err = nil default: - err = fmt.Errorf("unknown sub-command: %s", subcmd) + err = fmt.Errorf("%w: %s", ErrUnknownSubCommand, subcmd) } if err != nil { @@ -119,7 +125,7 @@ func networkNode(args []string) (string, string, error) { c := strings.SplitN(args[1], "/", 2) if len(c) == 1 { // no network in name if len(networks) == 0 { - return "", "", errors.New("no-existing Gont network") + return "", "", ErrNonExistingNetwork } network = networks[0] @@ -129,14 +135,14 @@ func networkNode(args []string) (string, string, error) { node = c[1] if !slices.Contains(networks, network) { - return "", "", fmt.Errorf("non-existing network '%s'", network) + return "", "", fmt.Errorf("%w: %s", ErrNonExistingNetwork, network) } } nodes := g.NodeNames(network) if !slices.Contains(nodes, node) { - return "", "", fmt.Errorf("non-existing node '%s' in network '%s'", node, network) + return "", "", fmt.Errorf("%w '%s' in network '%s'", ErrNonExistingNode, node, network) } return network, node, nil @@ -188,11 +194,11 @@ func clean(args []string) error { func exec(network, node string, args []string) error { if len(flag.Args()) <= 1 { - return fmt.Errorf("not enough arguments") + return ErrNotEnoughArguments } if network == "" { - return fmt.Errorf("there is no active Gont network") + return ErrNonExistingNetwork } if err := os.Setenv("GONT_NETWORK", network); err != nil { diff --git a/internal/execvpe/execvpe.go b/internal/execvpe/execvpe.go index b371121..35779d0 100644 --- a/internal/execvpe/execvpe.go +++ b/internal/execvpe/execvpe.go @@ -7,6 +7,7 @@ package execvpe // See: https://sourceware.org/git/?p=glibc.git;a=blob;f=posix/execvpe.c import ( + "errors" "os" "path/filepath" "strings" @@ -40,7 +41,7 @@ func Execvpe(argv0 string, argv []string, envp []string) error { if strings.Contains(argv0, "/") { err := syscall.Exec(argv0, argv, envp) - if err == syscall.ENOEXEC { + if errors.Is(err, syscall.ENOEXEC) { if err := maybeScriptExecute(argv0, argv, envp); err != nil { return err } @@ -57,34 +58,34 @@ func Execvpe(argv0 string, argv []string, envp []string) error { gotEacces := false for _, p := range strings.Split(path, ":") { argv0 := filepath.Join(p, argv0) - if err = syscall.Exec(argv0, argv, envp); err == syscall.ENOEXEC { + if err = syscall.Exec(argv0, argv, envp); errors.Is(err, syscall.ENOEXEC) { err = maybeScriptExecute(argv0, argv, envp) } - switch err { - case syscall.EACCES: + switch { + case errors.Is(err, syscall.EACCES): // Record that we got a 'Permission denied' error. If we end // up finding no executable we can use, we want to diagnose // that we did find one but were denied access. gotEacces = true - case syscall.ENOENT: + case errors.Is(err, syscall.ENOENT): fallthrough - case syscall.ESTALE: + case errors.Is(err, syscall.ESTALE): fallthrough - case syscall.ENOTDIR: + case errors.Is(err, syscall.ENOTDIR): fallthrough + // Those errors indicate the file is missing or not executable // by us, in which case we want to just try the next path // directory. - - case syscall.ENODEV: + case errors.Is(err, syscall.ENODEV): fallthrough - case syscall.ETIMEDOUT: + case errors.Is(err, syscall.ETIMEDOUT): + // Some strange file systems like AFS return even // stranger error numbers. They cannot reasonably mean // anything else so ignore those, too. - default: // Some other error means we found an executable file, but // something went wrong executing it; return the error to our diff --git a/internal/prque/prque.go b/internal/prque/prque.go index 49ecd6f..74f4a75 100644 --- a/internal/prque/prque.go +++ b/internal/prque/prque.go @@ -31,7 +31,7 @@ func (q heapl) Swap(i, j int) { } func (q *heapl) Push(x any) { - *q = append(*q, x.(Item)) + *q = append(*q, x.(Item)) //nolint:forcetypeassert } func (q *heapl) Pop() any { @@ -64,9 +64,7 @@ func (q *PriorityQueue) Pop() Item { q.lock.Lock() defer q.lock.Unlock() - item := heap.Pop(&q.heap).(Item) - - return item + return heap.Pop(&q.heap).(Item) //nolint:forcetypeassert } func (q *PriorityQueue) Oldest() time.Time { diff --git a/internal/utils/ip.go b/internal/utils/ip.go index 63ba9c4..f7b7118 100644 --- a/internal/utils/ip.go +++ b/internal/utils/ip.go @@ -12,12 +12,14 @@ import ( func ipToInt(ip net.IP) (*big.Int, int) { val := &big.Int{} val.SetBytes([]byte(ip)) - if len(ip) == net.IPv4len { + + switch { + case len(ip) == net.IPv4len: return val, 32 - } else if len(ip) == net.IPv6len { + case len(ip) == net.IPv6len: return val, 128 - } else { - panic(fmt.Errorf("Unsupported address length %d", len(ip))) + default: + panic(fmt.Sprintf("unsupported address length %d", len(ip))) } } diff --git a/pkg/capture.go b/pkg/capture.go index ad5e7bd..e010712 100644 --- a/pkg/capture.go +++ b/pkg/capture.go @@ -126,12 +126,15 @@ func NewCapture(opts ...CaptureOption) *Capture { // Count returns the total number of captured packets func (c *Capture) Count() uint64 { - return uint64(c.count.Load()) + return c.count.Load() } func (c *Capture) Flush() error { for c.queue.Len() > 0 { - p := c.queue.Pop().(CapturePacket) + p, ok := c.queue.Pop().(CapturePacket) + if !ok { + continue + } if err := c.writePacket(p); err != nil { return err @@ -239,7 +242,10 @@ out: break } - p := c.queue.Pop().(CapturePacket) + p, ok := c.queue.Pop().(CapturePacket) + if !ok { + continue + } if err := c.writePacket(p); err != nil { c.logger.Error("Failed to handle packet. Stop capturing...", zap.Error(err)) diff --git a/pkg/capture_interface.go b/pkg/capture_interface.go index 5912390..ba0d093 100644 --- a/pkg/capture_interface.go +++ b/pkg/capture_interface.go @@ -37,10 +37,10 @@ func (ci *captureInterface) readPackets(c *Capture) { if err := ci.readPacket(c); err != nil { if errors.Is(err, io.EOF) { break - } else { - c.logger.Error("Failed to read packet data", zap.Error(err)) - continue } + + c.logger.Error("Failed to read packet data", zap.Error(err)) + continue } } } diff --git a/pkg/capture_pcapgo.go b/pkg/capture_pcapgo.go index 3757b18..ef2be04 100644 --- a/pkg/capture_pcapgo.go +++ b/pkg/capture_pcapgo.go @@ -6,6 +6,7 @@ package gont import ( + "errors" "fmt" "github.com/gopacket/gopacket/layers" @@ -13,6 +14,8 @@ import ( "golang.org/x/net/bpf" ) +var ErrFilterExpressionsRequireCGO = errors.New("libpcap filter expressions require CGo") + const CGoPCAP = false type pcapgoPacketSource struct { @@ -25,7 +28,7 @@ func (c *Capture) createPCAPHandle(name string) (packetSource, error) { return nil, fmt.Errorf("failed to open PCAP handle: %w", err) } - if err := hdl.SetCaptureLength(int(c.SnapshotLength)); err != nil { + if err := hdl.SetCaptureLength(c.SnapshotLength); err != nil { return nil, fmt.Errorf("failed to set capture length: %w", err) } @@ -34,7 +37,7 @@ func (c *Capture) createPCAPHandle(name string) (packetSource, error) { } if c.FilterExpression != "" { - return nil, fmt.Errorf("libpcap filter expressions require CGo") + return nil, ErrFilterExpressionsRequireCGO } if c.FilterInstructions != nil { diff --git a/pkg/capture_test.go b/pkg/capture_test.go index 9e73f30..94ac66d 100644 --- a/pkg/capture_test.go +++ b/pkg/capture_test.go @@ -49,7 +49,7 @@ func TestCaptureNetwork(t *testing.T) { } }() - cb := func(p g.CapturePacket) { + cb := func(_ g.CapturePacket) { // fmt.Println("Callback", p.String()) } @@ -80,7 +80,12 @@ func TestCaptureNetwork(t *testing.T) { co.FilterPackets(func(p *g.CapturePacket) bool { pp := p.Decode(gopacket.DecodeOptions{}) if layer := pp.Layer(layers.LayerTypeICMPv6); layer != nil { - typec := layer.(*layers.ICMPv6).TypeCode.Type() + layer, ok := layer.(*layers.ICMPv6) + if !ok { + return false + } + + typec := layer.TypeCode.Type() return typec == layers.ICMPv6TypeEchoRequest || typec == layers.ICMPv6TypeEchoReply } @@ -131,7 +136,7 @@ func TestCaptureNetwork(t *testing.T) { h1veth0 := h1.Interface("veth0") h2veth0 := h2.Interface("veth0") - pkt, _, intf, eof := nextPacket(t, rd) + pkt, intf, eof := nextPacket(t, rd) require.Equal(t, eof, false, "Expected more packets") require.Equal(t, intf.Name, "h1/veth0", "Invalid 1st packet") @@ -151,35 +156,35 @@ func TestCaptureNetwork(t *testing.T) { h1veth0.Addresses[0].IP.String(), ) - _, _, intf, eof = nextPacket(t, rd) + _, intf, eof = nextPacket(t, rd) require.False(t, eof, "Expected more packets") require.Equal(t, intf.Name, "sw1/veth-h1", "Invalid 2nd packet") - _, _, intf, eof = nextPacket(t, rd) + _, intf, eof = nextPacket(t, rd) require.False(t, eof, "Expected more packets") require.Equal(t, intf.Name, "sw1/veth-h2", "Invalid 3rd packet") - _, _, intf, eof = nextPacket(t, rd) + _, intf, eof = nextPacket(t, rd) require.False(t, eof, "Expected more packets") require.Equal(t, intf.Name, "h2/veth0", "Invalid 4th packet") - _, _, intf, eof = nextPacket(t, rd) + _, intf, eof = nextPacket(t, rd) require.False(t, eof, "Expected more packets") require.Equal(t, intf.Name, "h2/veth0", "Invalid 5th packet") - _, _, intf, eof = nextPacket(t, rd) + _, intf, eof = nextPacket(t, rd) require.False(t, eof, "Expected more packets") require.Equal(t, intf.Name, "sw1/veth-h2", "Invalid 6th packet: %s", intf.Name) - _, _, intf, eof = nextPacket(t, rd) + _, intf, eof = nextPacket(t, rd) require.False(t, eof, "Expected more packets") require.Equal(t, intf.Name, "sw1/veth-h1", "Invalid 7th packet") - _, _, intf, eof = nextPacket(t, rd) + _, intf, eof = nextPacket(t, rd) require.False(t, eof, "Expected more packets") require.Equal(t, intf.Name, "h1/veth0", "Invalid 7th packet") - _, _, _, eof = nextPacket(t, rd) + _, _, eof = nextPacket(t, rd) require.True(t, eof, "Expected EOF") require.Equal(t, rd.NInterfaces(), 4, "Invalid number of interfaces") @@ -192,11 +197,11 @@ func TestCaptureNetwork(t *testing.T) { require.NoError(t, err, "Failed to close file") } -func nextPacket(t *testing.T, rd *pcapgo.NgReader) (gopacket.Packet, *gopacket.CaptureInfo, *pcapgo.NgInterface, bool) { +func nextPacket(t *testing.T, rd *pcapgo.NgReader) (gopacket.Packet, *pcapgo.NgInterface, bool) { data, ci, err := rd.ZeroCopyReadPacketData() if err != nil { if errors.Is(err, io.EOF) { - return nil, nil, nil, true + return nil, nil, true } require.NoError(t, err, "Failed to read packet data") @@ -205,5 +210,5 @@ func nextPacket(t *testing.T, rd *pcapgo.NgReader) (gopacket.Packet, *gopacket.C intf, err := rd.Interface(ci.InterfaceIndex) require.NoError(t, err, "Received packet from unknown interface") - return gopacket.NewPacket(data, layers.LinkTypeEthernet, gopacket.Default), &ci, &intf, false + return gopacket.NewPacket(data, layers.LinkTypeEthernet, gopacket.Default), &intf, false } diff --git a/pkg/cmd.go b/pkg/cmd.go index 4cc4c61..d0b1a78 100644 --- a/pkg/cmd.go +++ b/pkg/cmd.go @@ -6,6 +6,7 @@ package gont import ( "bytes" "context" + "errors" "fmt" "io" "log" @@ -19,6 +20,8 @@ import ( "go.uber.org/zap/zapio" ) +var ErrExitPrematurely = errors.New("process exited prematurely") + //nolint:gochecknoglobals var DefaultPreserveEnvVars = []string{ "PATH", @@ -76,8 +79,7 @@ func (n *NamespaceNode) Command(name string, args ...any) *Cmd { } for _, arg := range args { - switch arg := arg.(type) { - case ExecCmdOption: + if arg, ok := arg.(ExecCmdOption); ok { arg.ApplyExecCmd(c.Cmd) } } @@ -239,9 +241,9 @@ func (c *Cmd) tracer() *Tracer { return t } else if t := c.node.network.Tracer; t != nil { return t - } else { - return nil } + + return nil } func (c *Cmd) debugger() *Debugger { @@ -251,9 +253,9 @@ func (c *Cmd) debugger() *Debugger { return d } else if d := c.node.network.Debugger; d != nil { return d - } else { - return nil } + + return nil } func (c *Cmd) extraEnvFile(envName string, f *os.File) { @@ -344,7 +346,7 @@ func (c *Cmd) stoppedStart() error { zap.Int("trap_cause", ws.TrapCause())) if ws.Exited() { - return fmt.Errorf("process exited prematurely") + return ErrExitPrematurely } if !ws.Stopped() { @@ -371,6 +373,8 @@ func (c *Cmd) stoppedStart() error { c.logger.Debug("Detached from tracee") return nil + + default: } if err = syscall.PtraceCont(wpid, 0); err != nil { diff --git a/pkg/debug_instance.go b/pkg/debug_instance.go index b80cfa0..f0a9698 100644 --- a/pkg/debug_instance.go +++ b/pkg/debug_instance.go @@ -160,6 +160,8 @@ func (d *debuggerInstance) run() { case proc.StopManual: d.logger.Debug("Process stopped manually") return + + default: } } } @@ -353,8 +355,8 @@ func (d *debuggerInstance) createBreakpointsForLocation(tp *Tracepoint) error { } func ptrace(request int, pid int, addr uintptr, data uintptr) error { - if _, _, e1 := syscall.Syscall6(syscall.SYS_PTRACE, uintptr(request), uintptr(pid), uintptr(addr), uintptr(data), 0, 0); e1 != 0 { - return syscall.Errno(e1) + if _, _, e1 := syscall.Syscall6(syscall.SYS_PTRACE, uintptr(request), uintptr(pid), addr, data, 0, 0); e1 != 0 { + return e1 } return nil diff --git a/pkg/debug_instance_breakpoint.go b/pkg/debug_instance_breakpoint.go index 5c93111..ee2bcac 100644 --- a/pkg/debug_instance_breakpoint.go +++ b/pkg/debug_instance_breakpoint.go @@ -105,7 +105,7 @@ func (bpi *breakpointInstance) createWatchpoint(d *debuggerInstance, thr *api.Th return nil } -func (bpi *breakpointInstance) traceEvent(t *api.Thread, d *debuggerInstance) trace.Event { +func (bpi *breakpointInstance) traceEvent(t *api.Thread, _ *debuggerInstance) trace.Event { var msg string if bpi.message == nil { msg = fmt.Sprintf("Hit breakpoint %d: %s", bpi.ID, bpi.Name) diff --git a/pkg/debug_msg.go b/pkg/debug_msg.go index bcb3779..8b5aa15 100644 --- a/pkg/debug_msg.go +++ b/pkg/debug_msg.go @@ -8,12 +8,19 @@ package gont import ( + "errors" "fmt" "strings" "github.com/go-delve/delve/service/api" ) +var ( + ErrEmptyEvaluationString = errors.New("empty evaluation string") + ErrLogPointInvalidFormat = errors.New("invalid log point format") + ErrInvalidDebugMessage = errors.New("invalid debug message format") +) + type debugMessage struct { format string args []string @@ -39,7 +46,7 @@ func parseDebugMessage(msg string) (*debugMessage, error) { if braceCount--; braceCount == 0 { argStr := strings.TrimSpace(string(argSlice)) if len(argStr) == 0 { - return nil, fmt.Errorf("empty evaluation string") + return nil, ErrEmptyEvaluationString } args = append(args, argStr) formatSlice = append(formatSlice, '%', 's') @@ -53,7 +60,7 @@ func parseDebugMessage(msg string) (*debugMessage, error) { } else { switch r { case '}': - return nil, fmt.Errorf("invalid log point format, unexpected '}'") + return nil, fmt.Errorf("%w, unexpected '}'", ErrLogPointInvalidFormat) case '{': if braceCount++; braceCount == 1 { isArg, argSlice = true, []rune{} @@ -64,7 +71,7 @@ func parseDebugMessage(msg string) (*debugMessage, error) { } } if isArg || len(formatSlice) == 0 { - return nil, fmt.Errorf("invalid debug message format") + return nil, ErrInvalidDebugMessage } return &debugMessage{ diff --git a/pkg/debug_vscode.go b/pkg/debug_vscode.go index 1235bd4..f93ac73 100644 --- a/pkg/debug_vscode.go +++ b/pkg/debug_vscode.go @@ -20,11 +20,15 @@ import ( "go.uber.org/zap" ) +var ErrFailedToFindWorkspaceDir = errors.New("failed to find workspace directory") + +//nolint:tagliatelle type vscTasksConfig struct { Version string `json:"version"` Tasks []vscTask `json:"tasks"` } +//nolint:tagliatelle type vscTask struct { Label string `json:"label"` Type string `json:"type"` @@ -35,22 +39,26 @@ type vscTask struct { ProblemMatcher *vscProblemMatcher `json:"problemMatcher,omitempty"` } +//nolint:tagliatelle type vscProblemMatcher struct { Owner string `json:"owner,omitempty"` Pattern *vscPattern `json:"pattern,omitempty"` Background *vscBackground `json:"background,omitempty"` } +//nolint:tagliatelle type vscBackground struct { ActiveOnStart bool `json:"activeOnStart,omitempty"` BeginsPattern string `json:"beginsPattern,omitempty"` EndsPattern string `json:"endsPattern,omitempty"` } +//nolint:tagliatelle type vscPattern struct { Regexp string `json:"regexp,omitempty"` } +//nolint:tagliatelle type vscLaunchConfig struct { Version string `json:"version,omitempty"` Configurations []vscConfiguration `json:"configurations,omitempty"` @@ -63,6 +71,7 @@ type vscPresentation struct { Panel string `json:"panel,omitempty"` } +//nolint:tagliatelle type vscConfiguration struct { Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` @@ -75,6 +84,7 @@ type vscConfiguration struct { Mode string `json:"mode,omitempty"` } +//nolint:tagliatelle type vscCompound struct { Name string `json:"name,omitempty"` StopAll bool `json:"stopAll,omitempty"` @@ -88,7 +98,7 @@ type vscCompound struct { // instances // If an empty dir is passed, we attempt to find the workspace directory by searching for a // parent directory which contains either a .vscode, go.mod or .git -func (d *Debugger) WriteVSCodeConfigs(dir string, stopOnEntry bool) error { +func (d *Debugger) WriteVSCodeConfigs(dir string, _ bool) error { if dir == "" { wd, err := os.Getwd() if err != nil { @@ -97,7 +107,7 @@ func (d *Debugger) WriteVSCodeConfigs(dir string, stopOnEntry bool) error { var ok bool if dir, ok = findWorkspaceDir(wd); !ok { - return errors.New("failed to find workspace directory") + return ErrFailedToFindWorkspaceDir } } diff --git a/pkg/exec.go b/pkg/exec.go index 304320e..8916faf 100644 --- a/pkg/exec.go +++ b/pkg/exec.go @@ -22,6 +22,7 @@ const ( persNoRandomize = 0x0040000 // ADDR_NO_RANDOMIZE ) +//nolint:gochecknoinits func init() { unshare := os.Getenv("GONT_UNSHARE") node := os.Getenv("GONT_NODE") diff --git a/pkg/filter_test.go b/pkg/filter_test.go index 2a0a7bb..2ab31d2 100644 --- a/pkg/filter_test.go +++ b/pkg/filter_test.go @@ -14,6 +14,7 @@ import ( "golang.org/x/sys/unix" ) +//nolint:dupl func TestFilterIPv4(t *testing.T) { n, err := g.NewNetwork(*nname, globalNetworkOptions...) require.NoError(t, err, "Failed to create network") @@ -52,6 +53,7 @@ func TestFilterIPv4(t *testing.T) { require.Error(t, err, "Succeeded to ping h1") } +//nolint:dupl func TestFilterIPv6(t *testing.T) { n, err := g.NewNetwork(*nname, globalNetworkOptions...) require.NoError(t, err, "Failed to create network") diff --git a/pkg/gont.go b/pkg/gont.go index 7ceb1fc..2b8008d 100644 --- a/pkg/gont.go +++ b/pkg/gont.go @@ -22,6 +22,8 @@ const ( bridgeInterfaceName = "br" ) +var ErrMissingPrivileges = errors.New("missing NET_ADMIN capabilities") + // Option is the base type for all functional options. type Option any @@ -29,7 +31,7 @@ type Option any func CheckCaps() error { c := cap.GetProc() if v, err := c.GetFlag(cap.Effective, cap.SYS_ADMIN); err != nil || !v { - return errors.New("missing NET_ADMIN capabilities") + return ErrMissingPrivileges } return nil } diff --git a/pkg/host.go b/pkg/host.go index 62a7614..8a1e66b 100644 --- a/pkg/host.go +++ b/pkg/host.go @@ -116,14 +116,14 @@ func (h *Host) ConfigureInterface(i *Interface) error { if !i.EnableDAD { fn := filepath.Join("/proc/sys/net/ipv6/conf", i.Name, "accept_dad") if err := h.WriteProcFS(fn, "0"); err != nil { - return fmt.Errorf("failed to enabled IPv6 forwarding: %s", err) + return fmt.Errorf("failed to disable IPv6 DAD: %w", err) } } for _, addr := range i.Addresses { addr := addr if err := i.AddAddress(&addr); err != nil { - return fmt.Errorf("failed to add link address: %s", err) + return fmt.Errorf("failed to add link address: %w", err) } } @@ -132,7 +132,7 @@ func (h *Host) ConfigureInterface(i *Interface) error { func (h *Host) Traceroute(o *Host, opts ...any) error { if h.network != o.network { - return fmt.Errorf("hosts must be on same network") + return ErrDifferentNetworks } opts = append(opts, o) diff --git a/pkg/host_ping.go b/pkg/host_ping.go index 1419ce6..6bbb84c 100644 --- a/pkg/host_ping.go +++ b/pkg/host_ping.go @@ -12,6 +12,11 @@ import ( "go.uber.org/zap/zapio" ) +var ( + ErrFailedToFindAddress = errors.New("failed to find address") + ErrPacketsLost = errors.New("packets lost") +) + func (h *Host) Ping(o *Host) (*ping.Statistics, error) { return h.PingWithOptions(o, "ip", 1, 5*time.Second, time.Second, true) } @@ -31,13 +36,13 @@ func (h *Host) PingWithOptions(o *Host, net string, count int, timeout time.Dura p.Interval = intv if h.network != o.network { - return nil, fmt.Errorf("hosts must be on same network") + return nil, ErrDifferentNetworks } // Find first IP address of first interface ip := o.LookupAddress(net) if ip == nil { - return nil, errors.New("failed to find address") + return nil, ErrFailedToFindAddress } logger := h.logger.Named("pinger") @@ -82,7 +87,7 @@ func (h *Host) PingWithOptions(o *Host, net string, count int, timeout time.Dura } if lost := p.PacketsSent - p.PacketsRecv; lost > 0 { - err = fmt.Errorf("lost %d packets", lost) + err = fmt.Errorf("%w: %d", ErrPacketsLost, lost) } return p.Statistics(), err diff --git a/pkg/link.go b/pkg/link.go index 685a6d6..ebdfd6f 100644 --- a/pkg/link.go +++ b/pkg/link.go @@ -14,6 +14,13 @@ import ( "golang.org/x/sys/unix" ) +var ( + ErrDifferentNetworks = errors.New("nodes are belonging to different networks") + ErrMissingNode = errors.New("cant establish link between interfaces without node") + ErrInterfaceNameTooLong = errors.New("interface names are too long") + ErrAddLinkToSelf = errors.New("failed to link the node with itself") +) + type VethOption interface { ApplyVeth(ve *nl.Veth) } @@ -26,19 +33,19 @@ func (n *Network) AddLink(l, r *Interface, opts ...Option) error { var err error if len(l.Name) > syscall.IFNAMSIZ-1 || len(r.Name) > syscall.IFNAMSIZ-1 { - return fmt.Errorf("interface names are too long. max_len=%d", syscall.IFNAMSIZ-1) + return fmt.Errorf("%w. max_len=%d", ErrInterfaceNameTooLong, syscall.IFNAMSIZ-1) } if l.Node == nil || r.Node == nil { - return errors.New("cant establish link between interfaces without node") + return ErrMissingNode } if l.Node == r.Node { - return errors.New("failed to link the node with itself") + return ErrAddLinkToSelf } if l.Node.Network() != r.Node.Network() { - return errors.New("nodes are belonging to different networks") + return ErrDifferentNetworks } // Create Veth pair @@ -63,8 +70,7 @@ func (n *Network) AddLink(l, r *Interface, opts ...Option) error { // Apply options for _, opt := range opts { - switch opt := opt.(type) { - case VethOption: + if opt, ok := opt.(VethOption); ok { opt.ApplyVeth(veth) } } diff --git a/pkg/main_test.go b/pkg/main_test.go index 7a9e963..5915826 100644 --- a/pkg/main_test.go +++ b/pkg/main_test.go @@ -42,8 +42,7 @@ func setupLogging() *zap.Logger { } func TestMain(m *testing.M) { - logger := setupLogging() - defer logger.Sync() //nolint:errcheck + setupLogging() flag.Parse() diff --git a/pkg/nat.go b/pkg/nat.go index f6eceb2..5b21942 100644 --- a/pkg/nat.go +++ b/pkg/nat.go @@ -47,9 +47,8 @@ func (n *Network) AddNAT(name string, opts ...Option) (*NAT, error) { } // Apply NAT options - for _, o := range opts { - switch opt := o.(type) { - case NATOption: + for _, opt := range opts { + if opt, ok := opt.(NATOption); ok { opt.ApplyNAT(nat) } } @@ -63,7 +62,7 @@ func (n *Network) AddNAT(name string, opts ...Option) (*NAT, error) { return nat, nil } -func (n *Network) AddHostNAT(name string, opts ...Option) (*NAT, error) { +func (n *Network) AddHostNAT(_ string, opts ...Option) (*NAT, error) { host := n.HostNode if err := host.EnableForwarding(); err != nil { diff --git a/pkg/network.go b/pkg/network.go index 45b6a38..6ad8ce6 100644 --- a/pkg/network.go +++ b/pkg/network.go @@ -20,6 +20,8 @@ import ( "go.uber.org/zap" ) +var ErrCreateNode = errors.New("failed to create node") + type NetworkOption interface { ApplyNetwork(n *Network) } @@ -119,7 +121,7 @@ func NewNetwork(name string, opts ...NetworkOption) (*Network, error) { n.HostNode = HostNode(n) if n.HostNode == nil { - return nil, errors.New("failed to create host node") + return nil, ErrCreateNode } if err := n.GenerateHostsFile(); err != nil { @@ -286,7 +288,7 @@ func (n *Network) KeyLogPipe(secretsType uint32) (*os.File, error) { } if len(capturesWithKeys) == 0 { - return nil, nil + return nil, nil //nolint:nilnil } rd, wr, err := os.Pipe() diff --git a/pkg/network_files.go b/pkg/network_files.go index d76c32c..0f5b099 100644 --- a/pkg/network_files.go +++ b/pkg/network_files.go @@ -151,7 +151,7 @@ func readNSSwitchConfig(fn string) (map[string][]string, error) { return nil, err } - return nil, nil + return m, nil } func writeNSSwitchConfig(fn string, config map[string][]string) error { diff --git a/pkg/ns_node.go b/pkg/ns_node.go index 7dca4df..e45b729 100644 --- a/pkg/ns_node.go +++ b/pkg/ns_node.go @@ -126,7 +126,7 @@ func (n *Network) AddNamespaceNode(name string, opts ...Option) (*NamespaceNode, return nil, err } if err := unix.Mount(src, dst, "", syscall.MS_BIND, ""); err != nil { - return nil, fmt.Errorf("failed to bind mount netns fd: %s", err) + return nil, fmt.Errorf("failed to bind mount netns fd: %w", err) } n.Register(node) @@ -256,7 +256,7 @@ func (n *NamespaceNode) ConfigureInterface(i *Interface) error { n.Interfaces = append(n.Interfaces, i) if err := n.network.GenerateHostsFile(); err != nil { - return fmt.Errorf("failed to update hosts file") + return fmt.Errorf("failed to update hosts file: %w", err) } return nil diff --git a/pkg/options/bridge.go b/pkg/options/bridge.go index b452d8a..2a03ab7 100644 --- a/pkg/options/bridge.go +++ b/pkg/options/bridge.go @@ -26,7 +26,7 @@ func (vf VLANFiltering) ApplyBridge(b *nl.Bridge) { b.VlanFiltering = &v } -// AgingTime configures the bridge's FDB entries ageing time, ie the number of seconds a MAC address will be kept in the FDB after a packet has been received from that address. +// AgingTime configures the bridge's FDB entries aging time, ie the number of seconds a MAC address will be kept in the FDB after a packet has been received from that address. // After this time has passed, entries are cleaned up. type AgingTime time.Duration diff --git a/pkg/options/debug/tracepoint.go b/pkg/options/debug/tracepoint.go index c319482..3eba6d3 100644 --- a/pkg/options/debug/tracepoint.go +++ b/pkg/options/debug/tracepoint.go @@ -163,7 +163,7 @@ type UserData struct { } func (u UserData) ApplyTracepoint(b *g.Tracepoint) { - b.UserData = any(u.Data) + b.UserData = u.Data } func Data(d any) UserData { diff --git a/pkg/options/filters/network.go b/pkg/options/filters/network.go index 8c3e781..3a62142 100644 --- a/pkg/options/filters/network.go +++ b/pkg/options/filters/network.go @@ -36,7 +36,7 @@ func ipOffsetLen(ip net.IP, dir direction) (uint32, uint32) { } func network(dir direction, netw *net.IPNet) Statement { - offset, len := ipOffsetLen(netw.IP, dir) + offset, length := ipOffsetLen(netw.IP, dir) fromAddr, toAddr := utils.AddressRange(netw) return Statement{ @@ -45,7 +45,7 @@ func network(dir direction, netw *net.IPNet) Statement { DestRegister: 1, Base: expr.PayloadBaseNetworkHeader, Offset: offset, - Len: len, + Len: length, }, &expr.Range{ Op: expr.CmpOpEq, diff --git a/pkg/options/options.go b/pkg/options/options.go index 0bfba30..3aeb36a 100644 --- a/pkg/options/options.go +++ b/pkg/options/options.go @@ -6,9 +6,9 @@ package options // Customize clones and extends a list of options without altering the list of base options. func Customize[T any](opts []T, extraOptions ...T) []T { - new := make([]T, 0, len(opts)+len(extraOptions)) - new = append(new, opts...) - new = append(new, extraOptions...) + n := make([]T, 0, len(opts)+len(extraOptions)) + n = append(n, opts...) + n = append(n, extraOptions...) - return new + return n } diff --git a/pkg/switch.go b/pkg/switch.go index 1d31e35..670c92f 100644 --- a/pkg/switch.go +++ b/pkg/switch.go @@ -108,12 +108,12 @@ func (sw *Switch) ConfigureInterface(i *Interface) error { sw.logger.Info("Connecting interface to bridge master", zap.Any("intf", i)) br, err := sw.nlHandle.LinkByName(bridgeInterfaceName) if err != nil { - return fmt.Errorf("failed to find bridge intf: %s", err) + return fmt.Errorf("failed to find bridge interface: %w", err) } l, err := sw.nlHandle.LinkByName(i.Name) if err != nil { - return fmt.Errorf("failed to find new bridge interface: %s", err) + return fmt.Errorf("failed to find new bridge interface: %w", err) } // Attach interface to bridge diff --git a/pkg/trace.go b/pkg/trace.go index 92c31c3..6ec6f2d 100644 --- a/pkg/trace.go +++ b/pkg/trace.go @@ -113,7 +113,10 @@ func (t *Tracer) Start() error { func (t *Tracer) Flush() error { for t.queue.Len() > 0 { - p := t.queue.Pop().(trace.Event) + p, ok := t.queue.Pop().(trace.Event) + if !ok { + continue + } if err := t.writeEvent(p); err != nil { return err @@ -161,10 +164,10 @@ func (t *Tracer) Pipe() (*os.File, error) { if _, err := e.ReadFrom(rd); err != nil { if errors.Is(err, io.EOF) { break - } else { - t.logger.Warn("Failed to read tracepoint from log", zap.Error(err)) - continue } + + t.logger.Warn("Failed to read tracepoint from log", zap.Error(err)) + continue } t.newEvent(e) @@ -222,7 +225,10 @@ out: break } - e := t.queue.Pop().(trace.Event) + e, ok := t.queue.Pop().(trace.Event) + if !ok { + continue + } if err := t.writeEvent(e); err != nil { t.logger.Error("Failed to handle event. Stop tracing...", zap.Error(err)) diff --git a/pkg/trace/event.go b/pkg/trace/event.go index 7047d8c..51f950d 100644 --- a/pkg/trace/event.go +++ b/pkg/trace/event.go @@ -22,6 +22,7 @@ var ( em cbor.EncMode ) +//nolint:gochecknoinits func init() { dm, _ = cbor.DecOptions{ DefaultMapType: reflect.TypeOf(map[string]any{}), @@ -53,6 +54,7 @@ const ( type EventCallback func(e Event) +//nolint:tagliatelle type Event struct { Timestamp time.Time `cbor:"time" json:"time"` Type string `cbor:"type" json:"type"` diff --git a/pkg/trace/log.go b/pkg/trace/log.go index 72d617e..73a0bec 100644 --- a/pkg/trace/log.go +++ b/pkg/trace/log.go @@ -22,7 +22,7 @@ type traceCore struct { fields []zapcore.Field } -func (c *traceCore) Enabled(lvl zapcore.Level) bool { +func (c *traceCore) Enabled(_ zapcore.Level) bool { return eventWriter != nil || eventCallback != nil } diff --git a/pkg/trace/trace.go b/pkg/trace/trace.go index a8b3e89..f27b2cd 100644 --- a/pkg/trace/trace.go +++ b/pkg/trace/trace.go @@ -5,6 +5,7 @@ package trace import ( "bufio" + "errors" "fmt" "io" "os" @@ -16,6 +17,12 @@ import ( // The following functions are intended to by used used for instrumentation of Go code // which is started by gont.Node.{Start,StartWith,Run} +var ( + ErrTracingAlreadyEnabled = errors.New("tracing already enabled") + ErrTracingNotSupported = errors.New("tracing not supported") + ErrTracingNotRunning = errors.New("tracing not running") +) + //nolint:gochecknoglobals var ( eventCallback EventCallback @@ -25,12 +32,12 @@ var ( func Start(bufsize int) error { if eventWriter != nil { - return fmt.Errorf("tracing already enabled") + return ErrTracingAlreadyEnabled } traceFileName := os.Getenv("GONT_TRACEFILE") if traceFileName == "" { - return fmt.Errorf("tracing not supported. Missing GONT_TRACEFILE environment variable") + return fmt.Errorf("%w: Missing GONT_TRACEFILE environment variable", ErrTracingNotSupported) } var err error @@ -53,7 +60,7 @@ func StartWithCallback(cb EventCallback) { func Stop() error { if eventWriter == nil { - return fmt.Errorf("tracing not running") + return ErrTracingNotRunning } if bufferedTraceWriter, ok := eventWriter.(*bufio.Writer); ok { diff --git a/pkg/trace_test.go b/pkg/trace_test.go index 705fafc..5417726 100644 --- a/pkg/trace_test.go +++ b/pkg/trace_test.go @@ -136,7 +136,7 @@ func TestTraceLog(t *testing.T) { "number": int64(1234), // zap is adding zap.Int() as an int64 internally } - _, filename, _, _ := runtime.Caller(0) + _, filename, _, _ := runtime.Caller(0) //nolint:dogsled function := "cunicu.li/gont/v2/pkg_test.TestTraceLog" diff --git a/test/tracee1/main.go b/test/tracee1/main.go index a50b5f1..f72f061 100644 --- a/test/tracee1/main.go +++ b/test/tracee1/main.go @@ -13,7 +13,6 @@ func main() { if err := trace.Start(0); err != nil { log.Fatalf("Failed to start tracer: %s", err) } - defer trace.Stop() //nolint:errcheck myData := map[string]any{ "Hello": "World", @@ -22,4 +21,8 @@ func main() { if err := trace.PrintfWithData(myData, "This is my first trace message: %s", "Hurra"); err != nil { log.Fatalf("Failed to write trace: %s", err) } + + if err := trace.Stop(); err != nil { + log.Fatalf("Failed to stop trace: %s", err) + } } From 9a332e7d33e47760fc41dbb6d439df846966f38e Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 20:49:50 +0200 Subject: [PATCH 11/11] fix: Patching of /etc/nsswitch.conf Signed-off-by: Steffen Vogel --- pkg/network_files.go | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/pkg/network_files.go b/pkg/network_files.go index 0f5b099..0831f27 100644 --- a/pkg/network_files.go +++ b/pkg/network_files.go @@ -10,7 +10,6 @@ import ( "net" "os" "path/filepath" - "slices" "strings" "cunicu.li/gont/v2/internal/utils" @@ -141,8 +140,12 @@ func readNSSwitchConfig(fn string) (map[string][]string, error) { cols := strings.Split(line, ":") + if len(cols) < 2 { + continue // Skip empty lines + } + db := cols[0] - srcs := cols[1:] + srcs := strings.Fields(cols[1]) m[db] = srcs } @@ -180,12 +183,32 @@ func (n *Network) patchNSSConfFile() error { return fmt.Errorf("failed to read nsswitch.conf: %w", err) } - for db := range cfg { - if db == "hosts" { - cfg[db] = slices.DeleteFunc(cfg[db], func(src string) bool { - return !strings.HasPrefix(src, "resolve") && !strings.HasPrefix(src, "mymachines") && !strings.HasPrefix(src, "myhostname") - }) + keepService := func(svc string) bool { + return svc != "resolve" && svc != "mymachines" && svc != "myhostname" + } + + isAction := func(s string) bool { + return len(s) >= 1 && s[0] == '[' + } + + if fields, ok := cfg["hosts"]; ok { + var newFields []string + + for i := 0; i < len(fields); i++ { + keep := keepService(fields[i]) + if keep { + newFields = append(newFields, fields[i]) + } + + if i+1 < len(fields) && isAction(fields[i+1]) { + i++ + if keep { + newFields = append(newFields, fields[i+1]) + } + } } + + cfg["hosts"] = newFields } fn := filepath.Join(n.VarPath, "files/etc/nsswitch.conf")