Skip to content

Commit

Permalink
local-capture: Add initial multi network namespace capture support
Browse files Browse the repository at this point in the history
Signed-off-by: Chance Zibolski <chance.zibolski@gmail.com>
  • Loading branch information
chancez committed May 2, 2024
1 parent b0f4692 commit cbc9ae5
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 12 deletions.
14 changes: 7 additions & 7 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
type captureOpts struct {
Logger *slog.Logger
Interfaces []string
Netns string
NetNamespaces []string
Filter string
CaptureConfig capture.Config
OutputFile string
Expand All @@ -30,7 +30,7 @@ func newCaptureFlags() *pflag.FlagSet {
fs.BoolP("print", "P", false, "Output the packet summary/details, even if writing raw packet data using the -o option.")
fs.Uint64P("capture-count", "c", 0, "Number of packets to capture.")
fs.DurationP("capture-duration", "d", 0, "Duration to capture packets.")
fs.StringP("netns", "N", "", "Run the capture in the specified network namespace")
fs.StringSliceP("netns", "N", []string{}, "Run the capture in the specified network namespaces")
fs.String("k8s-pod", "", "Run the capture on the target k8s pod. Requires containerd. Must also set k8s-namespace.")
fs.String("k8s-namespace", "", "Run the capture on the target k8s pod in namespace. Requires containerd. Must also set k8s-pod.")
fs.String("log-level", "info", "Configure the log level.")
Expand Down Expand Up @@ -70,7 +70,7 @@ func getCaptureOpts(ctx context.Context, filter string, fs *pflag.FlagSet) (*cap
if err != nil {
return nil, err
}
netns, err := fs.GetString("netns")
netns, err := fs.GetStringSlice("netns")
if err != nil {
return nil, err
}
Expand All @@ -93,10 +93,10 @@ func getCaptureOpts(ctx context.Context, filter string, fs *pflag.FlagSet) (*cap
}

return &captureOpts{
Logger: log,
Interfaces: ifaces,
Netns: netns,
Filter: filter,
Logger: log,
Interfaces: ifaces,
NetNamespaces: netns,
Filter: filter,
CaptureConfig: capture.Config{
Filter: filter,
Snaplen: snaplen,
Expand Down
80 changes: 76 additions & 4 deletions cmd/local_capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ package cmd

import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"strings"

"github.com/chancez/capper/pkg/capture"
"github.com/chancez/capper/pkg/containerd"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
)

var localCaptureCmd = &cobra.Command{
Expand Down Expand Up @@ -38,7 +42,6 @@ func runLocalCapture(cmd *cobra.Command, args []string) error {
return err
}

var netns string
if captureOpts.K8sNamespace != "" && captureOpts.K8sPod != "" {
containerdSock := "/run/containerd/containerd.sock"
captureOpts.Logger.Debug("connecting to containerd", "addr", containerdSock)
Expand All @@ -49,18 +52,26 @@ func runLocalCapture(cmd *cobra.Command, args []string) error {
defer client.Close()

captureOpts.Logger.Debug("looking up k8s pod in containerd", "pod", captureOpts.K8sPod, "namespace", captureOpts.K8sNamespace)
netns, err = containerd.GetPodNetns(ctx, client, captureOpts.K8sPod, captureOpts.K8sNamespace)
netns, err := containerd.GetPodNetns(ctx, client, captureOpts.K8sPod, captureOpts.K8sNamespace)
if err != nil {
return fmt.Errorf("error getting pod namespace: %w", err)
}
if netns == "" {
return fmt.Errorf("could not find netns for pod '%s/%s'", captureOpts.K8sNamespace, captureOpts.K8sPod)
}
captureOpts.Logger.Debug("found netns for pod", "pod", captureOpts.K8sPod, "namespace", captureOpts.K8sNamespace, "netns", netns)
captureOpts.Netns = netns
captureOpts.NetNamespaces = append(captureOpts.NetNamespaces, netns)
}

if len(captureOpts.NetNamespaces) > 1 {
return localCaptureMultiNamespace(ctx, captureOpts.Logger, captureOpts.Interfaces, captureOpts.NetNamespaces, captureOpts.CaptureConfig, captureOpts.OutputFile, captureOpts.AlwaysPrint)
}

return localCapture(ctx, captureOpts.Logger, captureOpts.Interfaces, captureOpts.Netns, captureOpts.CaptureConfig, captureOpts.OutputFile, captureOpts.AlwaysPrint)
var netns string
if len(captureOpts.NetNamespaces) == 1 {
netns = captureOpts.NetNamespaces[0]
}
return localCapture(ctx, captureOpts.Logger, captureOpts.Interfaces, netns, captureOpts.CaptureConfig, captureOpts.OutputFile, captureOpts.AlwaysPrint)
}

// localCapture runs a packet capture and stores the output to the specified file or
Expand Down Expand Up @@ -105,6 +116,67 @@ func localCapture(ctx context.Context, log *slog.Logger, ifaces []string, netns
return nil
}

func localCaptureMultiNamespace(ctx context.Context, log *slog.Logger, ifaces []string, netNamespaces []string, conf capture.Config, outputDir string, alwaysPrint bool) error {
if len(netNamespaces) < 2 {
return errors.New("localCaptureMultiNamespace requires at least 2 namespaces")
}
fi, err := os.Stat(outputDir)
if err != nil {
return err
}
if !fi.IsDir() {
return fmt.Errorf("%s is not a directory, multi-namespace capture requires output-file to point to a directory", outputDir)
}

var eg errgroup.Group
for _, netns := range netNamespaces {
// Create a capture per netns
handle, err := newCapture(ctx, log, ifaces, netns, conf)
if err != nil {
return err
}
defer handle.Close()
linkType := handle.LinkType()

var handlers []capture.PacketHandler
if alwaysPrint || outputDir == "" {
handlers = append(handlers, capture.PacketPrinterHandler)
}
if outputDir != "" {
// store each capture into it's own file in the outputDirectory
// TODO: Get the interface/auto-detected interfaces
fileName := strings.Trim(strings.ReplaceAll(netns, "/", "-"), "-") + ".pcap"
f, err := os.Create(filepath.Join(outputDir, fileName))
if err != nil {
return fmt.Errorf("error opening output: %w", err)
}
defer f.Close()
writeHandler, err := capture.NewPcapWriterHandler(f, linkType, uint32(conf.Snaplen))
if err != nil {
return err
}
handlers = append(handlers, writeHandler)
}

eg.Go(func() error {
err = handle.Start(ctx, capture.ChainPacketHandlers(handlers...))
if err != nil {
return fmt.Errorf("error occurred while capturing packets: %w", err)
}
return nil
})
}

err = eg.Wait()
if errors.Is(err, context.Canceled) {
return nil
}
if err != nil {
return err
}
return nil
}

func newCapture(ctx context.Context, log *slog.Logger, ifaces []string, netns string, conf capture.Config) (capture.Capture, error) {
if len(ifaces) >= 2 {
return capture.NewMulti(ctx, log, ifaces, netns, conf)
Expand Down
10 changes: 9 additions & 1 deletion cmd/remote_capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,17 @@ func runRemoteCapture(cmd *cobra.Command, args []string) error {
return err
}

var netns string
if len(captureOpts.NetNamespaces) > 1 {
return errors.New("remote capture only supports a single netns")
}
if len(captureOpts.NetNamespaces) == 1 {
netns = captureOpts.NetNamespaces[0]
}

req := &capperpb.CaptureRequest{
Interface: captureOpts.Interfaces,
Netns: captureOpts.Netns,
Netns: netns,
Filter: captureOpts.Filter,
Snaplen: int64(captureOpts.CaptureConfig.Snaplen),
NumPackets: captureOpts.CaptureConfig.NumPackets,
Expand Down

0 comments on commit cbc9ae5

Please sign in to comment.