From 402df4dd8328786a0cd8e4cc0b5394a2caa3f943 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:17:40 +0200 Subject: [PATCH 1/3] feat: Introduce a NamespaceNode in addition to the BaseNode Signed-off-by: Steffen Vogel --- pkg/base_node.go | 323 +--------------------- 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 | 336 +++++++++++++++++++++++ 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, 419 insertions(+), 363 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 d81dc594..2b795f4e 100644 --- a/pkg/base_node.go +++ b/pkg/base_node.go @@ -5,17 +5,10 @@ package gont import ( "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 { @@ -23,139 +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) - } - - // 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 } @@ -178,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 96dc6641..a9fbbdfd 100644 --- a/pkg/capture.go +++ b/pkg/capture.go @@ -97,7 +97,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 02e56fbd..d4dd9666 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 73170b60..fb9b67f1 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 f53795aa..639d5a9c 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 { @@ -129,7 +126,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 d9ba419e..d9be2b5d 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 6cabfb62..701936ac 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 97d741d5..eec4a81b 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 bb11ccc6..1ee09306 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 26c54466..a4074abf 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 00000000..08a6f524 --- /dev/null +++ b/pkg/ns_node.go @@ -0,0 +1,336 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package gont + +import ( + "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) + } + + // 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 bf36c36e..95e3155d 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 dfee01de..3e77c4b5 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 d8a51c3b..1c41c8fa 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 455cf3fc..bfb32321 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 ea3e9b06..1d31e35a 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 6161d10d..92c31c39 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 e08051a0f1baa197ef2c6c99ffe4ff3707623927 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 19:15:53 +0200 Subject: [PATCH 2/3] fix: Linter warnings Signed-off-by: Steffen Vogel --- .golangci.yaml | 129 +++++++++++++++++++++++++++---- 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, 299 insertions(+), 126 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 580587f8..ef596ac0 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 + - 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 - copyloopvar - - gci - - gochecknoglobals - - gocognit - - gofmt - - misspell - - tagliatelle - - whitespace - - revive - - gosec - - nestif - - nolintlint - - prealloc + - 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 ed997888..bd2620f0 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 b371121e..35779d09 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 49ecd6f0..74f4a758 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 ef2faec4..3c81ee59 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 a9fbbdfd..123028ef 100644 --- a/pkg/capture.go +++ b/pkg/capture.go @@ -128,12 +128,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 @@ -254,7 +257,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 59123904..ba0d093d 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 3757b18c..ef2be043 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 9e73f307..94ac66d0 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 d4dd9666..891bbd10 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", @@ -80,8 +83,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) } } @@ -249,9 +251,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 { @@ -261,9 +263,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) { @@ -354,7 +356,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() { @@ -381,6 +383,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 d6d31e8e..90626c92 100644 --- a/pkg/debug_instance.go +++ b/pkg/debug_instance.go @@ -159,6 +159,8 @@ func (d *debuggerInstance) run() { case proc.StopManual: d.logger.Debug("Process stopped manually") return + + default: } } } @@ -352,8 +354,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 5c931112..ee2bcac1 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 bcb37792..8b5aa15a 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 1235bd4d..f93ac734 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 4e5d7df0..ad8c5c03 100644 --- a/pkg/exec.go +++ b/pkg/exec.go @@ -21,6 +21,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 2a0a7bbc..2ab31d23 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 7ceb1fc5..2b8008d4 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 639d5a9c..a90cb560 100644 --- a/pkg/host.go +++ b/pkg/host.go @@ -116,13 +116,13 @@ 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 { 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) } } @@ -131,7 +131,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 1419ce65..6bbb84ce 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 685a6d60..ebdfd6ff 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 7a9e9638..59158260 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 eec4a81b..60f942b2 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 1ee09306..0819ee55 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 b8c938ec..6fb35085 100644 --- a/pkg/network_files.go +++ b/pkg/network_files.go @@ -180,7 +180,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 08a6f524..eed220a8 100644 --- a/pkg/ns_node.go +++ b/pkg/ns_node.go @@ -117,7 +117,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) @@ -247,7 +247,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 b452d8a4..2a03ab75 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 c3194821..3eba6d34 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 8c3e781a..3a621425 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 0bfba300..3aeb36ab 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 1d31e35a..670c92f6 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 92c31c39..6ec6f2db 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 3a4fdf28..7b5a85cf 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 abeb16fa..39444bd2 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 a8b3e899..f27b2cd7 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 705fafc7..5417726e 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 a50b5f17..f72f0616 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 f12cb71364c5f4bc3981acac64968a86ae6204ca Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Tue, 27 Aug 2024 08:52:27 +0200 Subject: [PATCH 3/3] wip --- pkg/cloud-init-metadata.yaml | 2 + pkg/cloud-init-userdata.yaml | 4 + pkg/options/vm/vm.go | 96 +++++++++++++++++ pkg/vm.go | 195 +++++++++++++++++++++++++++++++++++ pkg/vm_test.go | 82 +++++++++++++++ 5 files changed, 379 insertions(+) create mode 100644 pkg/cloud-init-metadata.yaml create mode 100644 pkg/cloud-init-userdata.yaml create mode 100644 pkg/options/vm/vm.go create mode 100644 pkg/vm.go create mode 100644 pkg/vm_test.go diff --git a/pkg/cloud-init-metadata.yaml b/pkg/cloud-init-metadata.yaml new file mode 100644 index 00000000..5a38ba10 --- /dev/null +++ b/pkg/cloud-init-metadata.yaml @@ -0,0 +1,2 @@ +instance-id: h1 +local-hostname: h1 diff --git a/pkg/cloud-init-userdata.yaml b/pkg/cloud-init-userdata.yaml new file mode 100644 index 00000000..fdffea1c --- /dev/null +++ b/pkg/cloud-init-userdata.yaml @@ -0,0 +1,4 @@ +ssh_authorized_keys: + - ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBERI62l9pAbMxi6QYd3xnEMJhOY9NxcUOvgzNrJsDqSSRs5UgRjHCTDbw+7+yqr+ibcwDAcQgnzJEdRqsdhdTdc= +ssh_import_id: + - gh:stv0g diff --git a/pkg/options/vm/vm.go b/pkg/options/vm/vm.go new file mode 100644 index 00000000..5dee497e --- /dev/null +++ b/pkg/options/vm/vm.go @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package vm + +import ( + "fmt" + "strings" + + g "cunicu.li/gont/v2/pkg" + co "cunicu.li/gont/v2/pkg/options/cmd" +) + +type Architecture string + +func (a Architecture) ApplyQEmuVM(vm *g.QEmuVM) { + vm.Arch = string(a) +} + +func Option(name string, opts ...any) co.Arguments { + kvs := []string{} + + for _, opt := range opts { + switch opt := opt.(type) { + case map[string]any: + for key, value := range opt { + kvs = append(kvs, fmt.Sprintf("%s=%v", key, value)) + } + case map[string]string: + for key, value := range opt { + kvs = append(kvs, fmt.Sprintf("%s=%s", key, value)) + } + default: + kvs = append(kvs, fmt.Sprint(opt)) + } + } + + args := co.Arguments{"-" + name} + + if len(kvs) > 0 { + args = append(args, strings.Join(kvs, ",")) + } + + return args +} + +type CloudInitUserData map[string]any + +func (c CloudInitUserData) ApplyQEmuVM(vm *g.QEmuVM) { + vm.CloudInit.UserData = c +} + +type CloudInitMetaData map[string]any + +func (c CloudInitMetaData) ApplyQEmuVM(vm *g.QEmuVM) { + vm.CloudInit.MetaData = c +} + +// Shortcuts + +func Memory(megs int) co.Arguments { + return Option("m", map[string]any{"size": megs}) +} + +//nolint:gochecknoglobals +var NoGraphic = Option("nographic") + +func Machine(typ string, opts ...any) co.Arguments { + args := []any{typ} + args = append(args, opts...) + + return Option("machine", args...) +} + +func CPU(model string) co.Arguments { + return Option("cpu", model) +} + +func Device(driver string, props map[string]any) co.Arguments { + return Option("device", driver, props) +} + +func NetDev(typ string, props map[string]any) co.Arguments { + return Option("netdev", typ, props) +} + +func Drive(props map[string]any) co.Arguments { + return Option("drive", props) +} + +func VNC(display int, opts ...any) co.Arguments { + args := []any{fmt.Sprintf(":%d", display)} + args = append(args, opts...) + + return Option("vnc", args...) +} diff --git a/pkg/vm.go b/pkg/vm.go new file mode 100644 index 00000000..6fb704a5 --- /dev/null +++ b/pkg/vm.go @@ -0,0 +1,195 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package gont + +import ( + "errors" + "fmt" + "maps" + "os" + "path/filepath" + "slices" + + "gopkg.in/yaml.v3" +) + +var ErrMissingCloudInitData = errors.New("missing cloud-init user data") + +type QEmuVMOption interface { + ApplyQEmuVM(vm *QEmuVM) +} + +var _ Node = (*QEmuVM)(nil) + +type CloudInit struct { + MetaData map[string]any + UserData map[string]any + NetworkConfig map[string]any +} + +type QEmuVM struct { + *BaseNode + + options []any + command *Cmd + + // Options + Arch string + CloudInit CloudInit +} + +func (n *Network) AddQEmuVM(name string, opts ...Option) (*QEmuVM, error) { + baseNode, err := n.addBaseNode(name, opts) + if err != nil { + return nil, err + } + + vm := &QEmuVM{ + BaseNode: baseNode, + } + + n.Register(vm) + + // Apply VM options + for _, opt := range opts { + switch opt := opt.(type) { + case QEmuVMOption: + opt.ApplyQEmuVM(vm) + case ExecCmdOption, CmdOption: + vm.options = append(vm.options, opt) + } + } + + // Add links + if err := vm.configureLinks(); err != nil { + return nil, fmt.Errorf("failed to configure links: %w", err) + } + + return vm, nil +} + +func (vm *QEmuVM) Close() error { + if vm.command.Process != nil { + if err := vm.Stop(); err != nil { + return fmt.Errorf("failed to stop VM: %w", err) + } + } + + return nil +} + +func (vm *QEmuVM) configureLinks() error { + for range vm.ConfiguredInterfaces { + return errors.ErrUnsupported + } + + return nil +} + +func (vm *QEmuVM) ConfigureInterface(_ *Interface) error { + return errors.ErrUnsupported +} + +func (vm *QEmuVM) Teardown() error { + return errors.ErrUnsupported +} + +func (vm *QEmuVM) Start() (*Cmd, error) { + name := fmt.Sprintf("/nix/store/v3jjx86y2cs6x9182f3d1i3z1kbap876-qemu-8.2.3/bin/qemu-system-%s", vm.Arch) + + args := slices.Clone(vm.options) + + if vm.CloudInit.UserData != nil { + fn, err := vm.createCloudInitImage() + if err != nil { + return nil, fmt.Errorf("failed to create cloud-init seed image: %w", err) + } + + args = append(args, "-drive", fmt.Sprintf("if=virtio,format=raw,file=%s", fn)) + } + + vm.command = vm.network.HostNode.Command(name, args...) + vm.command.Stdin = os.Stdin + vm.command.Stderr = os.Stderr + vm.command.Stdout = os.Stdout + + if err := vm.command.Start(); err != nil { + return nil, err + } + + return vm.command, nil +} + +func (vm *QEmuVM) Stop() error { + // TODO: Perform orderly shutdown of VM + return vm.command.Process.Kill() +} + +func (vm *QEmuVM) createCloudInitImage() (string, error) { + if vm.CloudInit.UserData == nil { + return "", ErrMissingCloudInitData + } + + fnOut := filepath.Join(vm.BasePath, "cloud-init.img") + + args := []any{ + "--disk-format", "raw", + "--filesystem", "iso9660", + fnOut, + } + + out := []byte("#cloud-config\n") + outYAML, err := yaml.Marshal(vm.CloudInit.UserData) + if err != nil { + return "", fmt.Errorf("failed to marshal cloud-init metadata: %w", err) + } + + out = append(out, outYAML...) + + fnUser := filepath.Join(vm.BasePath, "cloud-init-userdata.yaml") + if err := os.WriteFile(fnUser, out, 0o600); err != nil { + return "", fmt.Errorf("failed to write file: %s: %w", fnUser, err) + } + + args = append(args, fnUser) + + if vm.CloudInit.MetaData != nil { + meta := maps.Clone(vm.CloudInit.MetaData) + meta["dsmode"] = "local" + + out, err := yaml.Marshal(meta) + if err != nil { + return "", fmt.Errorf("failed to marshal cloud-init metadata: %w", err) + } + + fnMeta := filepath.Join(vm.BasePath, "cloud-init-metadata.yaml") + if err := os.WriteFile(fnMeta, out, 0o600); err != nil { + return "", fmt.Errorf("failed to write file: %s: %w", fnMeta, err) + } + + args = append(args, fnMeta) + } + + if vm.CloudInit.NetworkConfig != nil { + out, err := yaml.Marshal(vm.CloudInit.NetworkConfig) + if err != nil { + return "", fmt.Errorf("failed to marshal cloud-init network config: %w", err) + } + + fnNetCfg := filepath.Join(vm.BasePath, "cloud-init-network.yaml") + if err := os.WriteFile(fnNetCfg, out, 0o600); err != nil { + return "", fmt.Errorf("failed to write file: %s: %w", fnNetCfg, err) + } + + // args = append(args, fnNetCfg) + } + + cmd := vm.network.HostNode.Command("/nix/store/05dsd12j15sg8qbf7jz6dg5kv7q32z1c-cloud-utils-0.32/bin/cloud-localds", args...) + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("failed to run cloud-localds: %w", err) + } + + return fnOut, nil +} diff --git a/pkg/vm_test.go b/pkg/vm_test.go new file mode 100644 index 00000000..dbb30cb5 --- /dev/null +++ b/pkg/vm_test.go @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 Steffen Vogel +// SPDX-License-Identifier: Apache-2.0 + +package gont_test + +import ( + "testing" + + g "cunicu.li/gont/v2/pkg" + vmo "cunicu.li/gont/v2/pkg/options/vm" + "github.com/stretchr/testify/require" +) + +const imageURL = "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-arm64.qcow2" //nolint:unused + +// TestPing performs and end-to-end ping test +// between two hosts on a switched topology +// +// h1 <-> sw1 <-> h2 +func TestQEmuVM(t *testing.T) { + t.Skip() + + n, err := g.NewNetwork(*nname, globalNetworkOptions...) + require.NoError(t, err, "Failed to create network") + defer n.Close() + + sw1, err := n.AddSwitch("sw1") + require.NoError(t, err, "Failed to add switch") + + vmOpts := []g.Option{ + vmo.Architecture("x86_64"), + vmo.Machine("q35"), + // vmo.CPU("host"), + vmo.Memory(2 * 1024), + + vmo.NoGraphic, + vmo.VNC(0), + + vmo.Device("virtio-blk-pci", map[string]any{"drive": "disk"}), + + vmo.Device("virtio-net-pci", map[string]any{"netdev": "net0"}), + vmo.NetDev("tap", map[string]any{"id": "net0", "script": "no", "downscript": "no"}), + vmo.Drive(map[string]any{"if": "none", "id": "disk", "format": "qcow2", "file": "/home/stv0g/workspace/cunicu/gont/image.qcow2"}), + + g.NewInterface("veth0", sw1), + + vmo.CloudInitMetaData{ + "instance-id": "h1", + "local-hostname": "h1", + }, + + vmo.CloudInitUserData{ + "ssh_pwauth": true, + "users": []any{ + "default", + map[string]any{ + "name": "stv0g", + "ssh_authorized_keys": []string{ + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBERI62l9pAbMxi6QYd3xnEMJhOY9NxcUOvgzNrJsDqSSRs5UgRjHCTDbw+7+yqr+ibcwDAcQgnzJEdRqsdhdTdc=", + }, + "ssh_import_id": []string{ + "gh:stv0g", + }, + "groups": []string{"users", "admin", "wheel"}, + "plain_text_passwd": "testtest", + }, + }, + }, + } + + h1, err := n.AddQEmuVM("h1", vmOpts...) + require.NoError(t, err, "Failed to add VM") + + // h2, err := n.AddQEmuVM("h2", vmOpts...) + // require.NoError(t, err, "Failed to add VM") + + cmd, err := h1.Start() + require.NoError(t, err) + + err = cmd.Wait() + require.NoError(t, err) +}