From ab7c9bf36891862690d2c936749f91c4089a0753 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sat, 15 Jun 2024 18:17:40 +0200 Subject: [PATCH] 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 3e63cb67..2b795f4e 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 29ff8158..6937820b 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 6f77714a..900bd717 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 221be72d..45b6a387 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..7dca4dfc --- /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 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 }