capture

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 23 Imported by: 0

README

Packet capture

Import github.com/JSYoo5B/ceph-testcontainers-go/capture to record the packets of one container endpoint around selected test traffic, so that a test can check afterwards what went over the wire. The package starts an opt-in tcpdump helper in the target's network namespace, retrieves one classic pcap file and checks its structure. It never decodes or judges the packets; interpreting Messenger traffic is the test's job. See API purpose and purpose tags for where this fits.

Order of use

  1. Start a target. Any running container on a user-defined bridge: an owned Ceph daemon (for example a MON of a ceph.Run cluster) or a container the test owns.
  2. Start the capture with capture.Start(ctx, target, capture.Request{...}). It returns once tcpdump listens; start the traffic of interest only after that.
  3. Run the SUT so that it sends the traffic to observe.
  4. Stop and check with Session.Stop, which returns a capture.Report (how the capture ended, whether the target changed, the file's structural verdict). Call Stop also after a failed workload; it is retryable. Alternatively register the session with Finalization.Capture (metadata only) or Finalization.CaptureWithFile (raw file into a failure archive), which settles it at the end of the test.
session, err := capture.Start(ctx, monitor, capture.Request{
	HelperImage: helperImage, // an image that meets packet-capture contract 1
	Network:     networkName,
	Interface:   "eth0",
	Filter:      "tcp port 3300",
	SnapLen:     65535,
	MaxPackets:  10000,
	Output:      filepath.Join(t.TempDir(), "mon.pcap"),
})
if err != nil {
	t.Fatal(err)
}
runWorkload(t)
report, err := session.Stop(ctx)
if err != nil {
	t.Fatal(err)
}
if report.End != capture.EndStopped || report.File.Status != capture.FileValid {
	t.Fatalf("capture did not cover the workload: %+v", report)
}

Concepts and limits

  • One endpoint, one file: a capture observes one container's interface on one user-defined bridge. bridge, host and none networks, any/lo interfaces, host-network targets and targets that share another container's namespace are refused. There is no rotation and no host-network capture.
  • The helper image is the caller's choice. It must meet packet-capture contract 1; no helper image is published yet, so nothing selects one implicitly. The helper runs as the image's non-root user with NET_RAW as its only capability.
  • A capture is bound to the target's container instance and network sandbox at readiness and never follows a restart; Report.Target says whether the target changed.
  • Process, target, retrieval and file outcomes are reported separately. A structurally valid file does not show that the intended traffic was captured, that nothing was dropped, or that any message is valid. A capture that ended at MaxPackets is not an error, but later traffic is missing.
  • The caller owns the session and its output file: no cluster Terminate stops it. A pcap holds payloads and identifiers that redaction does not sanitize; keep it only where the test deliberately retains it.

Reference

The full contract of every type and function is in go doc: pkg.go.dev or go doc -all github.com/JSYoo5B/ceph-testcontainers-go/capture. Error categories are described in Errors; failure archives in Contexts and cleanup.

Documentation

Overview

Package capture records packets in one container's network namespace with an opt-in packet-capture helper, retrieves the classic pcap file and checks its structure. It never interprets the packets: a structurally valid file does not show that the intended traffic was captured, that nothing was dropped, or that any protocol message in it is valid.

The helper meets contract 1 of the packet-capture extension of ceph-testcontainers-images. No helper image is published yet, so callers always name the image; the package never selects one by itself.

Index

Constants

View Source
const (
	// MaxSnapLen bounds Request.SnapLen: libpcap's largest default snaplen.
	MaxSnapLen = 262144
	// MaxFileBytes bounds the capture file that a request allows:
	// 24 + MaxPackets × (16 + SnapLen) bytes, as the helper contract derives
	// it.
	MaxFileBytes = 1 << 30
	// MaxFilterBytes bounds Request.Filter.
	MaxFilterBytes = 4096
	// DefaultReadyTimeout is the readiness wait of a zero Request.ReadyTimeout.
	DefaultReadyTimeout = 30 * time.Second
	// DefaultStopGrace is the graceful-stop wait of a zero Request.StopGrace.
	DefaultStopGrace = 10 * time.Second
	// MaxTimeout bounds Request.ReadyTimeout and Request.StopGrace.
	MaxTimeout = 5 * time.Minute
)

Variables

View Source
var (
	// ErrImageUnsupported reports a helper image that does not meet the
	// capture contract: a missing sh, sleep, tcpdump or python3, a tcpdump
	// that does not run or compile a filter, or a helper that runs as root.
	// It belongs to ceph.ErrImageCapability.
	ErrImageUnsupported error = &categoryError{text: "the capture helper image does not meet the capture contract", kind: ceph.ErrImageCapability}
	// ErrPermissionDenied reports an engine that did not let the helper's
	// tcpdump run or open its device with NET_RAW. It belongs to
	// ceph.ErrEnvironmentUnavailable.
	ErrPermissionDenied error = &categoryError{text: "the capture helper was not permitted to capture packets", kind: ceph.ErrEnvironmentUnavailable}
	// ErrFailed reports a capture that did not start, end or deliver as the
	// contract says: tcpdump exited before readiness or with a nonzero
	// status, did not end within the stop grace, the target's network
	// namespace changed, or the helper's file could not be copied or is not
	// a valid classic pcap. It belongs to ceph.ErrNativeFailure.
	ErrFailed error = &categoryError{text: "the packet capture failed", kind: ceph.ErrNativeFailure}
)

Functions

This section is empty.

Types

type EndReason

type EndReason string

EndReason says how the capture process ended.

const (
	// EndStopped means tcpdump ended with status 0 after Stop's SIGTERM.
	EndStopped EndReason = "stopped"
	// EndPacketLimit means tcpdump ended with status 0 before Stop, and the
	// valid file holds Request.MaxPackets packets (as does tcpdump's
	// captured counter when it printed one): later traffic is not in the
	// file.
	EndPacketLimit EndReason = "packet_limit"
	// EndPremature means tcpdump ended with status 0 before Stop, but the
	// file and its counter do not show that it reached Request.MaxPackets,
	// as after a signal from elsewhere: traffic after its end is missing.
	EndPremature EndReason = "premature"
	// EndKilled means tcpdump did not end within the stop grace and the
	// helper was killed; the file may hold only part of the capture.
	EndKilled EndReason = "killed"
	// EndFailed means tcpdump ended with a nonzero status.
	EndFailed EndReason = "failed"
	// EndUnknown means the end could not be observed, or that tcpdump ended
	// with status 0 before Stop and no valid file was retrieved to tell
	// EndPacketLimit from EndPremature.
	EndUnknown EndReason = "unknown"
)

type FileReport

type FileReport struct {
	Status FileStatus `json:"status"`
	// Reason explains any status other than FileValid.
	Reason string `json:"reason,omitempty"`
	// ByteOrder is "little" or "big"; TimestampUnit is "microsecond" or
	// "nanosecond".
	ByteOrder     string `json:"byte_order,omitempty"`
	TimestampUnit string `json:"timestamp_unit,omitempty"`
	VersionMajor  uint16 `json:"version_major,omitempty"`
	VersionMinor  uint16 `json:"version_minor,omitempty"`
	SnapLen       uint32 `json:"snaplen,omitempty"`
	LinkType      uint32 `json:"link_type,omitempty"`
	Packets       int    `json:"packets"`
	// TruncatedPackets counts packets captured shorter than their original
	// length: snaplen truncation, not a malformed file.
	TruncatedPackets int `json:"truncated_packets"`
	// BytesRead is how much of the file was checked, and SHA256 is of exactly
	// those bytes. Complete says the check reached the end of the file, so
	// that SHA256 identifies the whole file.
	BytesRead int64  `json:"bytes_read"`
	SHA256    string `json:"sha256"`
	Complete  bool   `json:"complete"`
}

FileReport is the structural check of the retrieved file. Counts cover the records read completely before the verdict.

The check reads with two bounds, the file size and one record's captured length. A record's declared length is compared with them before anything is read, and record data streams through the digest without being held, so a hostile length can neither allocate nor read its declared size. Read errors are returned as errors, never as a verdict.

type FileStatus

type FileStatus string

FileStatus is the structural verdict on the retrieved file.

const (
	// FileValid means every record header and its data fit the file. A
	// valid file can hold zero packets.
	FileValid FileStatus = "valid"
	// FileUnsupported means a recognized format that is not checked, such
	// as pcapng.
	FileUnsupported FileStatus = "unsupported"
	// FileMalformed means the file is not classic pcap or breaks its
	// structure, including a final record that was cut off.
	FileMalformed FileStatus = "malformed"
	// FileLimitExceeded means the helper's file is larger than the
	// request's bound; only the first MaxBytes were kept and checked.
	FileLimitExceeded FileStatus = "limit_exceeded"
)

type Report

type Report struct {
	// ContainerID, Network, Interface and MACAddress identify the captured
	// endpoint; SandboxID, TargetStartedAt and Namespace the network
	// namespace generation that the helper joined.
	ContainerID     string `json:"container_id"`
	Network         string `json:"network"`
	Interface       string `json:"interface"`
	MACAddress      string `json:"mac_address"`
	SandboxID       string `json:"sandbox_id"`
	TargetStartedAt string `json:"target_started_at"`
	Namespace       string `json:"namespace,omitempty"`
	// HelperImage is the requested image, HelperImageID the image the helper
	// ran, HelperPlatform its os/architecture and ToolVersion what
	// tcpdump --version printed.
	HelperImage    string `json:"helper_image"`
	HelperImageID  string `json:"helper_image_id"`
	HelperPlatform string `json:"helper_platform,omitempty"`
	ToolVersion    string `json:"tool_version,omitempty"`
	// Filter, SnapLen, MaxPackets and MaxBytes are the requested selection
	// and bounds; MaxBytes is derived from SnapLen and MaxPackets.
	Filter     string `json:"filter"`
	SnapLen    int    `json:"snaplen"`
	MaxPackets int    `json:"max_packets"`
	MaxBytes   int64  `json:"max_bytes"`
	// StartedAt is when the helper was started, ReadyAt when tcpdump
	// reported that it listens and its file existed, StopRequestedAt when
	// Stop began and EndedAt when the capture process ended.
	StartedAt       time.Time `json:"started_at"`
	ReadyAt         time.Time `json:"ready_at"`
	StopRequestedAt time.Time `json:"stop_requested_at"`
	EndedAt         time.Time `json:"ended_at"`
	// End and ExitCode say how tcpdump ended.
	End      EndReason `json:"end"`
	ExitCode int       `json:"exit_code"`
	// Target compares the target at Stop with the generation joined.
	Target TargetState `json:"target"`
	// Statistics are tcpdump's counters; Stderr is its standard error, up
	// to 64 KiB.
	Statistics Statistics `json:"statistics"`
	Stderr     string     `json:"stderr,omitempty"`
	// Output is the delivered file, Retrieved whether the helper's file was
	// copied into it and RetrievedBytes how many bytes it holds.
	Output         string `json:"output"`
	Retrieved      bool   `json:"retrieved"`
	RetrievedBytes int64  `json:"retrieved_bytes"`
	// RetrievedSHA256 identifies all bytes successfully delivered to Output,
	// independently of the structural check's possibly shorter prefix.
	RetrievedSHA256 string `json:"retrieved_sha256,omitempty"`
	// File is the structural check of Output; its Status is empty when
	// nothing was retrieved.
	File FileReport `json:"file"`
	// HelperRemoved says the helper container was removed.
	HelperRemoved bool `json:"helper_removed"`
}

Report describes one capture: what was requested, what the helper and the target did, and what was delivered. Process, target, retrieval and file outcomes are separate: a valid file can come from a capture that ended early or lost its target.

The report carries JSON field tags for delivery. The pcap itself holds payloads, addresses and identifiers that text and JSON redaction do not sanitize; keep capture files only where the caller deliberately selects them.

type Request

type Request struct {
	// HelperImage is an image that meets contract 1 of the packet-capture
	// extension.
	HelperImage string
	// Network is the user-defined bridge network through which the target's
	// endpoint is selected; bridge, host and none are refused.
	Network string
	// Interface is the target's interface on Network, inside the target's
	// network namespace, such as "eth0". Start checks that its MAC address
	// is that of the endpoint; any and lo are refused.
	Interface string
	// Filter is a pcap-filter expression that selects the traffic. It is
	// compiled before the capture starts.
	Filter string
	// SnapLen bounds the bytes kept of each packet: 1 to MaxSnapLen.
	SnapLen int
	// MaxPackets ends the capture after that many packets. With SnapLen it
	// bounds the file to 24 + MaxPackets × (16 + SnapLen) bytes, at most
	// MaxFileBytes.
	MaxPackets int
	// Output is the path of the file that Stop writes the capture to. It must
	// not exist; Start creates it, and its directory must exist, so parallel
	// or repeated tests need distinct paths.
	Output string
	// ReadyTimeout bounds the wait for readiness; zero is
	// DefaultReadyTimeout.
	ReadyTimeout time.Duration
	// StopGrace bounds the wait for tcpdump to end after SIGTERM; zero is
	// DefaultStopGrace. After it, Stop kills the helper.
	StopGrace time.Duration
}

Request selects one capture. Every field except the two timeouts is required: the package chooses no image, interface, filter or bound.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session owns one running capture helper. The caller owns the session and calls Stop, also after a failed workload: no cluster Terminate knows it. Stop is retryable.

func Start

func Start(ctx context.Context, target testcontainers.Container, req Request) (*Session, error)

Start starts a capture in target's network namespace, on its endpoint on req.Network, and returns once tcpdump listens there. It is an opt-in extension: req.HelperImage names the capture helper, which joins the target's namespace (Docker network mode container:<target>) with NET_RAW as its only capability and runs as the image's own non-root user. The target keeps its image, capabilities and networking; it may be an owned Ceph daemon or a caller-owned container attached to that bridge. Host-network, stopped or paused targets and targets that share another container's namespace are refused.

Before the capture starts, a short-lived probe from the same image in the same namespace checks the contract's executables (sh, sleep, tcpdump, python3) and non-root user, runs tcpdump --version, compiles the filter with tcpdump -y EN10MB -d and reads the interface's MAC address. The capture then runs the image ID that the probe ran, not the tag, as

tcpdump -p -i <Interface> -s <SnapLen> -c <MaxPackets> --immediate-mode -U -w <file> -- <Filter>

A missing HelperImage is ceph.ErrImageCapability; a bad request, filter or interface is ceph.ErrInvalidRequest; and the errors ErrImageUnsupported, ErrPermissionDenied and ErrFailed tell an unsuitable image, a denied capability and a capture that did not become ready apart.

Readiness is tcpdump's "listening on <Interface>, link-type EN10MB" line together with its output file; a running helper or an existing file alone is not. Start the workload of interest only after Start returns. The session is bound to the target's container instance and sandbox at that point and never follows a restart. After a failure Start removes the helper and the output file it created. It returns a non-nil session with an error only when it could not remove a probe or a helper that failed; the empty output file then stays, and Stop retries the removal.

func (*Session) CaptureFile

func (s *Session) CaptureFile(ctx context.Context) (io.ReadCloser, int64, string, error)

CaptureFile opens the settled session's original, caller-owned output for read-only archive copying. The size and SHA256 identify all retrieved bytes, not merely the structural validator's prefix. It does not stop the session or transfer ownership of its file. Close the returned reader.

Finalization.CaptureWithFile is the explicit consent to archive these raw, unsanitized packets; ordinary Finalization.Capture never opens this source. The archive copies only for a failure: it verifies EOF, the exact size and the full SHA256 before it publishes the copy as capture-<registration>-<name>.pcap, refuses a symlink or other non-regular source, and fails verification when the file changed after retrieval. Keep the file and its directory until the finalization returns: a t.TempDir registered after TerminateOnCleanup is removed first, so create it before registering the finalization or use t.ArtifactDir.

func (*Session) Settle

func (s *Session) Settle(ctx context.Context) (any, error)

Settle is Stop for a ceph.Finalization, which registers the session with Finalization.Capture. It returns the Report as an any value and, so that an archived report names no host path, with Output reduced to its base name. It shares Stop's behavior: the context's cancellation does not shorten it, it waits for a Stop that is already running, and a Stop that already finished is not repeated.

func (*Session) SettlementAllowance

func (s *Session) SettlementAllowance() time.Duration

SettlementAllowance is the longest Settle runs: the stop grace plus the package's own two-minute Docker allowance (130 seconds with the default stop grace), whatever context Settle is given. It is what ceph.Finalization.Capture admits for this session. Callers that do not use a finalization call Stop instead of Settle.

func (*Session) Stop

func (s *Session) Stop(ctx context.Context) (Report, error)

Stop ends the capture with SIGTERM and waits up to the stop grace for tcpdump to flush and close its file; after the grace it kills the helper (EndKilled). It then compares the target with the joined generation, copies the helper's file into Request.Output, checks its structure and removes the helper. Its Docker work uses a fresh bounded context, so a canceled or expired ctx does not leave the helper behind.

The report is returned also with an error. The error joins ErrFailed for a capture that failed, was killed, ended early on its own, lost its target or delivered a file that is not valid classic pcap, and a removal error. A capture that ended at MaxPackets is not an error; its End says so. After a removal error Stop can be called again; it returns the same report.

type Statistics

type Statistics struct {
	Captured           *uint64 `json:"captured,omitempty"`
	ReceivedByFilter   *uint64 `json:"received_by_filter,omitempty"`
	DroppedByKernel    *uint64 `json:"dropped_by_kernel,omitempty"`
	DroppedByInterface *uint64 `json:"dropped_by_interface,omitempty"`
}

Statistics are tcpdump's counters from its standard error. A nil field is a counter that tcpdump did not print: unknown, not zero.

type TargetState

type TargetState string

TargetState compares the target at Stop with the one the capture joined.

const (
	// TargetUnchanged means the same container instance and network sandbox.
	TargetUnchanged TargetState = "unchanged"
	// TargetChanged means the target restarted, stopped or got another
	// sandbox: the capture covers only the namespace it joined.
	TargetChanged TargetState = "changed"
	// TargetGone means the target container no longer exists.
	TargetGone TargetState = "gone"
	// TargetUnknown means the target could not be inspected.
	TargetUnknown TargetState = "unknown"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL