-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[client, proxy] Add packet capture to debug bundle and CLI #5891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lixmal
wants to merge
5
commits into
main
Choose a base branch
from
add-packet-capture
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e58c29d
Add packet capture to debug bundle and CLI
lixmal b734534
Refactor embed capture API to StartCapture/StopCapture
lixmal 635343f
Merge branch 'main' into add-packet-capture
lixmal 3da5c7a
Fix race and error handling in capture flow
lixmal 33e166f
Remove unused getCaptureEngine
lixmal File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "os/signal" | ||
| "path/filepath" | ||
| "strings" | ||
| "syscall" | ||
|
|
||
| "github.com/hashicorp/go-multierror" | ||
| "github.com/spf13/cobra" | ||
| "google.golang.org/grpc/status" | ||
| "google.golang.org/protobuf/types/known/durationpb" | ||
|
|
||
| nberrors "github.com/netbirdio/netbird/client/errors" | ||
| "github.com/netbirdio/netbird/client/proto" | ||
| "github.com/netbirdio/netbird/util/capture" | ||
| ) | ||
|
|
||
| var captureCmd = &cobra.Command{ | ||
| Use: "capture", | ||
| Short: "Capture packets on the WireGuard interface", | ||
| Long: `Captures decrypted packets flowing through the WireGuard interface. | ||
|
|
||
| Default output is human-readable text. Use --pcap or --output for pcap binary. | ||
| Requires --enable-capture to be set at service install or reconfigure time. | ||
|
|
||
| Examples: | ||
| netbird debug capture | ||
| netbird debug capture host 100.64.0.1 and port 443 | ||
| netbird debug capture tcp | ||
| netbird debug capture icmp | ||
| netbird debug capture src host 10.0.0.1 and dst port 80 | ||
| netbird debug capture -o capture.pcap | ||
| netbird debug capture --pcap | tshark -r - | ||
| netbird debug capture --pcap | tcpdump -r - -n`, | ||
| Args: cobra.ArbitraryArgs, | ||
| RunE: runCapture, | ||
| } | ||
|
|
||
| func init() { | ||
| debugCmd.AddCommand(captureCmd) | ||
|
|
||
| captureCmd.Flags().Bool("pcap", false, "Force pcap binary output (default when --output is set)") | ||
| captureCmd.Flags().BoolP("verbose", "v", false, "Show seq/ack, TTL, window, total length") | ||
| captureCmd.Flags().Bool("ascii", false, "Print payload as ASCII after each packet (useful for HTTP)") | ||
| captureCmd.Flags().Uint32("snap-len", 0, "Max bytes per packet (0 = full)") | ||
| captureCmd.Flags().DurationP("duration", "d", 0, "Capture duration (0 = until interrupted)") | ||
| captureCmd.Flags().StringP("output", "o", "", "Write pcap to file instead of stdout") | ||
| } | ||
|
|
||
| func runCapture(cmd *cobra.Command, args []string) error { | ||
| conn, err := getClient(cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| defer func() { | ||
| if err := conn.Close(); err != nil { | ||
| cmd.PrintErrf(errCloseConnection, err) | ||
| } | ||
| }() | ||
|
|
||
| client := proto.NewDaemonServiceClient(conn) | ||
|
|
||
| req, err := buildCaptureRequest(cmd, args) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM) | ||
| defer cancel() | ||
|
|
||
| stream, err := client.StartCapture(ctx, req) | ||
| if err != nil { | ||
| return handleCaptureError(err) | ||
| } | ||
|
|
||
| // First Recv is the empty acceptance message from the server. If the | ||
| // device is unavailable (kernel WG, not connected, capture disabled), | ||
| // the server returns an error instead. | ||
| if _, err := stream.Recv(); err != nil { | ||
| return handleCaptureError(err) | ||
| } | ||
|
|
||
| out, cleanup, err := captureOutput(cmd) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if req.TextOutput { | ||
| cmd.PrintErrf("Capturing packets... Press Ctrl+C to stop.\n") | ||
| } else { | ||
| cmd.PrintErrf("Capturing packets (pcap)... Press Ctrl+C to stop.\n") | ||
| } | ||
|
|
||
| streamErr := streamCapture(ctx, cmd, stream, out) | ||
| cleanupErr := cleanup() | ||
| if streamErr != nil { | ||
| return streamErr | ||
| } | ||
| return cleanupErr | ||
| } | ||
|
|
||
| func buildCaptureRequest(cmd *cobra.Command, args []string) (*proto.StartCaptureRequest, error) { | ||
| req := &proto.StartCaptureRequest{} | ||
|
|
||
| if len(args) > 0 { | ||
| expr := strings.Join(args, " ") | ||
| if _, err := capture.ParseFilter(expr); err != nil { | ||
| return nil, fmt.Errorf("invalid filter: %w", err) | ||
| } | ||
| req.FilterExpr = expr | ||
| } | ||
|
|
||
| if snap, _ := cmd.Flags().GetUint32("snap-len"); snap > 0 { | ||
| req.SnapLen = snap | ||
| } | ||
| if d, _ := cmd.Flags().GetDuration("duration"); d != 0 { | ||
| if d < 0 { | ||
| return nil, fmt.Errorf("duration must not be negative") | ||
| } | ||
| req.Duration = durationpb.New(d) | ||
| } | ||
| req.Verbose, _ = cmd.Flags().GetBool("verbose") | ||
| req.Ascii, _ = cmd.Flags().GetBool("ascii") | ||
|
|
||
| outPath, _ := cmd.Flags().GetString("output") | ||
| forcePcap, _ := cmd.Flags().GetBool("pcap") | ||
| req.TextOutput = !forcePcap && outPath == "" | ||
|
|
||
| return req, nil | ||
| } | ||
|
|
||
| func streamCapture(ctx context.Context, cmd *cobra.Command, stream proto.DaemonService_StartCaptureClient, out io.Writer) error { | ||
| for { | ||
| pkt, err := stream.Recv() | ||
| if err != nil { | ||
| if ctx.Err() != nil { | ||
| cmd.PrintErrf("\nCapture stopped.\n") | ||
| return nil //nolint:nilerr // user interrupted | ||
| } | ||
| if err == io.EOF { | ||
| cmd.PrintErrf("\nCapture finished.\n") | ||
| return nil | ||
| } | ||
| return handleCaptureError(err) | ||
| } | ||
| if _, err := out.Write(pkt.GetData()); err != nil { | ||
| return fmt.Errorf("write output: %w", err) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // captureOutput returns the writer for capture data and a cleanup function | ||
| // that finalizes the file. Errors from the cleanup must be propagated. | ||
| func captureOutput(cmd *cobra.Command) (io.Writer, func() error, error) { | ||
| outPath, _ := cmd.Flags().GetString("output") | ||
| if outPath == "" { | ||
| return os.Stdout, func() error { return nil }, nil | ||
| } | ||
|
|
||
| f, err := os.CreateTemp(filepath.Dir(outPath), filepath.Base(outPath)+".*.tmp") | ||
| if err != nil { | ||
| return nil, nil, fmt.Errorf("create output file: %w", err) | ||
| } | ||
| tmpPath := f.Name() | ||
| return f, func() error { | ||
| var merr *multierror.Error | ||
| if err := f.Close(); err != nil { | ||
| merr = multierror.Append(merr, fmt.Errorf("close output file: %w", err)) | ||
| } | ||
| fi, statErr := os.Stat(tmpPath) | ||
| if statErr != nil || fi.Size() == 0 { | ||
| if rmErr := os.Remove(tmpPath); rmErr != nil && !os.IsNotExist(rmErr) { | ||
| merr = multierror.Append(merr, fmt.Errorf("remove empty output file: %w", rmErr)) | ||
| } | ||
| return nberrors.FormatErrorOrNil(merr) | ||
| } | ||
| if err := os.Rename(tmpPath, outPath); err != nil { | ||
| merr = multierror.Append(merr, fmt.Errorf("rename output file: %w", err)) | ||
| return nberrors.FormatErrorOrNil(merr) | ||
| } | ||
| cmd.PrintErrf("Wrote %s\n", outPath) | ||
| return nberrors.FormatErrorOrNil(merr) | ||
| }, nil | ||
| } | ||
|
|
||
| func handleCaptureError(err error) error { | ||
| if s, ok := status.FromError(err); ok { | ||
| return fmt.Errorf("%s", s.Message()) | ||
| } | ||
| return err | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.