Skip to content

Commit

Permalink
feat: implement packet capture API
Browse files Browse the repository at this point in the history
This uses the `go-packet` library with native bindings for the packet
capture (without `libpcap`). This is not the most performant way, but it
allows us to avoid CGo.

There is a problem with converting network filter expressions (like
`tcp port 3222`) into BPF instructions, it's only available in C
libraries, but there's a workaround with `tcpdump`.

Signed-off-by: Andrey Smirnov <[email protected]>
  • Loading branch information
smira committed Jul 18, 2022
1 parent 7c006ca commit 065b592
Show file tree
Hide file tree
Showing 15 changed files with 1,522 additions and 313 deletions.
20 changes: 20 additions & 0 deletions api/machine/machine.proto
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ service MachineService {
rpc Version(google.protobuf.Empty) returns (VersionResponse);
// GenerateClientConfiguration generates talosctl client configuration (talosconfig).
rpc GenerateClientConfiguration(GenerateClientConfigurationRequest) returns (GenerateClientConfigurationResponse);
// PacketCapture performs packet capture and streams back pcap file.
rpc PacketCapture(PacketCaptureRequest) returns (stream common.Data);
}

// rpc applyConfiguration
Expand Down Expand Up @@ -1044,3 +1046,21 @@ message GenerateClientConfiguration {
message GenerateClientConfigurationResponse {
repeated GenerateClientConfiguration messages = 1;
}

message PacketCaptureRequest {
// Interface name to perform packet capture on.
string interface = 1;
// Enable promiscuous mode.
bool promiscuous = 2;
// Snap length in bytes.
uint32 snap_len = 3;
// BPF filter.
repeated BPFInstruction bpf_filter = 4;
}

message BPFInstruction {
uint32 op = 1;
uint32 jt = 2;
uint32 jf = 3;
uint32 k = 4;
}
196 changes: 196 additions & 0 deletions cmd/talosctl/cmd/talos/pcap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package talos

import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"time"

"github.com/google/gopacket"
"github.com/google/gopacket/pcapgo"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"

"github.com/talos-systems/talos/cmd/talosctl/pkg/talos/helpers"
"github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/client"
)

var pcapCmdFlags struct {
iface string
promisc bool
snaplen int
output string
bpfFilter string
duration time.Duration
}

// pcapCmd represents the pcap command.
var pcapCmd = &cobra.Command{
Use: "pcap",
Aliases: []string{"tcpdump"},
Short: "Capture the network packets from the node.",
Long: `The command launches packet capture on the node and streams back the packets as raw pcap file.
Default behavior is to decode the packets with internal decoder to stdout:
talosctl pcap -i eth0
Raw pcap file can be saved with --output flag:
talosctl pcap -i eth0 --output eth0.pcap
Output can be piped to tcpdump:
talosctl pcap -i eth0 -o - | tcpdump -vvv -r -
BPF filter can be applied, but it has to compiled to BPF instructions first using tcpdump.
Correct link type should be specified for the tcpdump: EN10MB for Ethernet links and RAW
for e.g. Wireguard tunnels:
talosctl pcap -i eth0 --bpf-filter "$(tcpdump -dd -y EN10MB 'tcp and dst port 80')"
talosctl pcap -i kubespan --bpf-filter "$(tcpdump -dd -y RAW 'port 50000')"
As packet capture is transmitted over the network, it is recommended to filter out the Talos API traffic,
e.g. by excluding packets with the port 50000.
`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return WithClient(func(ctx context.Context, c *client.Client) error {
if err := helpers.FailIfMultiNodes(ctx, "pcap"); err != nil {
return err
}

if pcapCmdFlags.duration > 0 {
var cancel context.CancelFunc

ctx, cancel = context.WithTimeout(ctx, pcapCmdFlags.duration)
defer cancel()
}

req := machine.PacketCaptureRequest{
Interface: pcapCmdFlags.iface,
Promiscuous: pcapCmdFlags.promisc,
SnapLen: uint32(pcapCmdFlags.snaplen),
}

var err error

req.BpfFilter, err = parseBPFInstructions(pcapCmdFlags.bpfFilter)
if err != nil {
return err
}

r, errCh, err := c.PacketCapture(ctx, &req)
if err != nil {
return fmt.Errorf("error copying: %w", err)
}

var wg sync.WaitGroup

wg.Add(1)
go func() {
defer wg.Done()
for err := range errCh {
fmt.Fprintln(os.Stderr, err.Error())
}
}()

defer wg.Wait()

if pcapCmdFlags.output == "" {
return dumpPackets(ctx, r)
}

var out io.Writer

if pcapCmdFlags.output == "-" {
out = os.Stdout
} else {
out, err = os.Create(pcapCmdFlags.output)
if err != nil {
return err
}
}

_, err = io.Copy(out, r)

if errors.Is(err, io.EOF) || client.StatusCode(err) == codes.DeadlineExceeded {
err = nil
}

return err
})
},
}

func dumpPackets(ctx context.Context, r io.Reader) error {
src, err := pcapgo.NewReader(r)
if err != nil {
return fmt.Errorf("error opening pcap reader: %w", err)
}

packetSource := gopacket.NewPacketSource(src, src.LinkType())

for packet := range packetSource.Packets() {
fmt.Println(packet)
}

return nil
}

// parseBPFInstructions parses the BPF raw instructions in 'tcpdump -dd' format.
//
// Example:
// { 0x30, 0, 0, 0x00000000 },
// { 0x54, 0, 0, 0x000000f0 },
// { 0x15, 0, 8, 0x00000060 },
func parseBPFInstructions(in string) ([]*machine.BPFInstruction, error) {
in = strings.TrimSpace(in)

if in == "" {
return nil, nil
}

var result []*machine.BPFInstruction //nolint:prealloc

for _, line := range strings.Split(in, "\n") {
if line == "" {
continue
}

ins := &machine.BPFInstruction{}

n, err := fmt.Sscanf(line, "{ 0x%x, %d, %d, 0x%x },", &ins.Op, &ins.Jt, &ins.Jf, &ins.K)
if err != nil {
return nil, fmt.Errorf("error parsing bpf instruction %q: %w", line, err)
}

if n != 4 {
return nil, fmt.Errorf("error parsing bpf instruction %q: expected 4 fields, got %d", line, n)
}

result = append(result, ins)
}

return result, nil
}

func init() {
pcapCmd.Flags().StringVarP(&pcapCmdFlags.iface, "interface", "i", "eth0", "interface name to capture packets on")
pcapCmd.Flags().BoolVar(&pcapCmdFlags.promisc, "promiscuous", false, "put interface into promiscuous mode")
pcapCmd.Flags().IntVarP(&pcapCmdFlags.snaplen, "snaplen", "s", 65536, "maximum packet size to capture")
pcapCmd.Flags().StringVarP(&pcapCmdFlags.output, "output", "o", "", "if not set, decode packets to stdout; if set write raw pcap data to a file, use '-' for stdout")
pcapCmd.Flags().StringVar(&pcapCmdFlags.bpfFilter, "bpf-filter", "", "bpf filter to apply, tcpdump -dd format")
pcapCmd.Flags().DurationVar(&pcapCmdFlags.duration, "duration", 0, "duration of the capture")
addCommand(pcapCmd)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ require (
github.com/godbus/dbus/v5 v5.1.0
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.8
github.com/google/gopacket v1.1.19
github.com/google/nftables v0.0.0-20220611213346-a346d51f53b3
github.com/google/uuid v1.3.0
github.com/gosuri/uiprogress v0.0.1
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,8 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
Expand Down
8 changes: 8 additions & 0 deletions hack/release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ talosctl gen config --with-secrets secrets.yaml my-cluster https://172.20.0.1:64
```
"""

[notes.packet-capture]
title = "Packet Capture"
description="""\
Talos now supports capturing packets on a network interface with `talosctl pcap` command:
talosctl pcap --interface eth0
"""

[make_deps]

[make_deps.tools]
Expand Down
1 change: 1 addition & 0 deletions internal/app/apid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ func Main() {
"/machine.MachineService/Kubeconfig",
"/machine.MachineService/List",
"/machine.MachineService/Logs",
"/machine.MachineService/PacketCapture",
"/machine.MachineService/Read",
"/resource.ResourceService/List",
"/resource.ResourceService/Watch",
Expand Down
Loading

0 comments on commit 065b592

Please sign in to comment.