Skip to content

Commit 4a616d6

Browse files
committed
Support antctl command for packetcapture
Signed-off-by: Hang Yan <[email protected]>
1 parent 7a2e3b9 commit 4a616d6

File tree

7 files changed

+410
-1
lines changed

7 files changed

+410
-1
lines changed

go.mod

+2-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ require (
7272
k8s.io/apiextensions-apiserver v0.31.1
7373
k8s.io/apimachinery v0.31.1
7474
k8s.io/apiserver v0.31.1
75+
k8s.io/cli-runtime v0.31.1
7576
k8s.io/client-go v0.31.1
7677
k8s.io/component-base v0.31.1
7778
k8s.io/klog/v2 v2.130.1
@@ -131,6 +132,7 @@ require (
131132
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
132133
github.com/evanphx/json-patch/v5 v5.9.0 // indirect
133134
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
135+
github.com/fatih/camelcase v1.0.0 // indirect
134136
github.com/felixge/httpsnoop v1.0.4 // indirect
135137
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
136138
github.com/go-errors/errors v1.4.2 // indirect
@@ -242,7 +244,6 @@ require (
242244
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
243245
gopkg.in/inf.v0 v0.9.1 // indirect
244246
gopkg.in/ini.v1 v1.67.0 // indirect
245-
k8s.io/cli-runtime v0.31.1 // indirect
246247
k8s.io/kms v0.31.1 // indirect
247248
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
248249
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0
212212
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
213213
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM=
214214
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
215+
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
216+
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
215217
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
216218
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
217219
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=

pkg/antctl/antctl.go

+6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
checkinstallation "antrea.io/antrea/pkg/antctl/raw/check/installation"
2424
"antrea.io/antrea/pkg/antctl/raw/featuregates"
2525
"antrea.io/antrea/pkg/antctl/raw/multicluster"
26+
"antrea.io/antrea/pkg/antctl/raw/packetcapture"
2627
"antrea.io/antrea/pkg/antctl/raw/proxy"
2728
"antrea.io/antrea/pkg/antctl/raw/set"
2829
"antrea.io/antrea/pkg/antctl/raw/supportbundle"
@@ -750,6 +751,11 @@ $ antctl get podmulticaststats pod -n namespace`,
750751
supportAgent: true,
751752
supportController: true,
752753
},
754+
{
755+
cobraCommand: packetcapture.Command,
756+
supportAgent: true,
757+
supportController: true,
758+
},
753759
{
754760
cobraCommand: proxy.Command,
755761
supportAgent: false,
+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package packetcapture
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net"
8+
"path/filepath"
9+
"strings"
10+
"time"
11+
12+
"github.com/spf13/cobra"
13+
v1 "k8s.io/api/core/v1"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/apimachinery/pkg/util/intstr"
16+
"k8s.io/apimachinery/pkg/util/rand"
17+
"k8s.io/apimachinery/pkg/util/wait"
18+
"k8s.io/utils/ptr"
19+
20+
"antrea.io/antrea/pkg/antctl/raw"
21+
"antrea.io/antrea/pkg/apis/crd/v1alpha1"
22+
)
23+
24+
const defaultTimeout time.Duration = time.Second * 60
25+
26+
var Command *cobra.Command
27+
28+
var option = &struct {
29+
source string
30+
dest string
31+
nowait bool
32+
timeout time.Duration
33+
number int32
34+
flow string
35+
}{}
36+
37+
var packetCaptureExample = strings.TrimSpace(`
38+
Start capture packets from pod1 to pod2, both Pods are in Namespace default
39+
$ antctl packetcaputre -S pod1 -D pod2
40+
Start capture packets from pod1 in Namespace ns1 to a destination IP
41+
$ antctl packetcapture -S ns1/pod1 -D 192.168.123.123
42+
`)
43+
44+
func init() {
45+
Command = &cobra.Command{
46+
Use: "packetcapture",
47+
Short: "Start capture packets",
48+
Long: "Start capture packets on the target flow.",
49+
Example: packetCaptureExample,
50+
RunE: packetCaptureRunE,
51+
}
52+
53+
Command.Flags().StringVarP(&option.source, "source", "S", "", "source of the the PacketCapture: Namespace/Pod, Pod, or IP")
54+
Command.Flags().StringVarP(&option.dest, "destination", "D", "", "destination of the PacketCapture: Namespace/Pod, Pod, or IP")
55+
Command.Flags().Int32VarP(&option.number, "number", "n", 0, "target packets number")
56+
Command.Flags().StringVarP(&option.flow, "flow", "f", "", "specify the flow (packet headers) of the PacketCapture , including tcp_src, tcp_dst , udp_src, udp_dst")
57+
Command.Flags().BoolVarP(&option.nowait, "nowait", "", false, "TODO")
58+
}
59+
60+
func packetCaptureRunE(cmd *cobra.Command, args []string) error {
61+
option.timeout, _ = cmd.Flags().GetDuration("timeout")
62+
if option.timeout > time.Hour {
63+
return errors.New("Timeout cannot be longer than 1 hour")
64+
}
65+
if option.timeout == 0 {
66+
option.timeout = defaultTimeout
67+
}
68+
if option.number == 0 {
69+
return errors.New("Packet number should be larger than 0")
70+
}
71+
72+
_, client, err := raw.GetClients(cmd)
73+
if err != nil {
74+
return err
75+
}
76+
pc, err := newPacketCapture()
77+
if err != nil {
78+
return fmt.Errorf("error when filling up PacketCapture config: %w", err)
79+
}
80+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
81+
defer cancel()
82+
83+
if _, err := client.CrdV1alpha1().PacketCaptures().Create(ctx, pc, metav1.CreateOptions{}); err != nil {
84+
return fmt.Errorf("error when creating PacketCapture, is PacketCapture feature gate enabled? %w", err)
85+
}
86+
defer func() {
87+
if !option.nowait {
88+
if err = client.CrdV1alpha1().PacketCaptures().Delete(context.TODO(), pc.Name, metav1.DeleteOptions{}); err != nil {
89+
fmt.Fprintf(cmd.OutOrStdout(), "error when deleting PacketCapture: %s", err.Error())
90+
}
91+
}
92+
}()
93+
94+
if option.nowait {
95+
fmt.Fprintf(cmd.OutOrStdout(), "PacketCapture Name: %s", pc.Name)
96+
return nil
97+
}
98+
99+
var latestPC *v1alpha1.PacketCapture
100+
err = wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, option.timeout, false, func(ctx context.Context) (bool, error) {
101+
res, err := client.CrdV1alpha1().PacketCaptures().Get(ctx, pc.Name, metav1.GetOptions{})
102+
if err != nil {
103+
return false, err
104+
}
105+
for _, cond := range res.Status.Conditions {
106+
if cond.Type == v1alpha1.PacketCaptureComplete && cond.Status == metav1.ConditionTrue {
107+
latestPC = res
108+
return true, nil
109+
}
110+
}
111+
return false, nil
112+
113+
})
114+
115+
if wait.Interrupted(err) {
116+
err = errors.New("timeout waiting for PacketCapture done")
117+
if latestPC == nil {
118+
return err
119+
}
120+
} else if err != nil {
121+
return fmt.Errorf("error when retrieving PacketCapture: %w", err)
122+
}
123+
124+
splits := strings.Split(latestPC.Status.FilePath, ":")
125+
// err checked before
126+
restConfig, _ := raw.ResolveKubeconfig(cmd)
127+
coreV1Client, err := initCoreV1Client(restConfig)
128+
if err != nil {
129+
return err
130+
}
131+
fileName := filepath.Base(splits[1])
132+
133+
pod := podFile{
134+
namespace: "kube-system",
135+
name: splits[0],
136+
containerName: "antrea-agent",
137+
restConfig: restConfig,
138+
coreClient: coreV1Client,
139+
}
140+
err = pod.copyFromPod(context.TODO(), splits[1], fileName)
141+
if err == nil {
142+
fmt.Fprintf(cmd.OutOrStdout(), "Packet File: %s\n", fileName)
143+
}
144+
return err
145+
146+
}
147+
148+
func parseEndpoint(endpoint string) (pod *v1alpha1.PodReference, ip *string) {
149+
parsedIP := net.ParseIP(endpoint)
150+
if parsedIP != nil && parsedIP.To4() != nil {
151+
ip = ptr.To(parsedIP.String())
152+
} else {
153+
split := strings.Split(endpoint, "/")
154+
if len(split) == 1 {
155+
pod = &v1alpha1.PodReference{
156+
Namespace: "default",
157+
Name: split[0],
158+
}
159+
} else if len(split) == 2 && len(split[0]) != 0 && len(split[1]) != 0 {
160+
pod = &v1alpha1.PodReference{
161+
Namespace: split[0],
162+
Name: split[1],
163+
}
164+
}
165+
}
166+
return
167+
}
168+
169+
func getPCName(src, dest string) string {
170+
replace := func(s string) string {
171+
return strings.ReplaceAll(s, "/", "-")
172+
}
173+
prefix := fmt.Sprintf("%s-%s", replace(src), replace(dest))
174+
if option.nowait {
175+
return prefix
176+
}
177+
return fmt.Sprintf("%s-%s", prefix, rand.String(8))
178+
}
179+
180+
func parseFlow() (*v1alpha1.Packet, error) {
181+
cleanFlow := strings.ReplaceAll(option.flow, " ", "")
182+
fields, err := raw.GetFlowFields(cleanFlow)
183+
if err != nil {
184+
return nil, fmt.Errorf("error when parsing the flow: %w", err)
185+
}
186+
var pkt v1alpha1.Packet
187+
pkt.IPFamily = v1.IPv4Protocol
188+
for k, v := range raw.Protocols {
189+
if _, ok := fields[k]; ok {
190+
pkt.Protocol = ptr.To(intstr.FromInt32(v))
191+
break
192+
}
193+
}
194+
if r, ok := fields["tcp_src"]; ok {
195+
pkt.TransportHeader.TCP = new(v1alpha1.TCPHeader)
196+
pkt.TransportHeader.TCP.SrcPort = ptr.To(int32(r))
197+
}
198+
if r, ok := fields["tcp_dst"]; ok {
199+
if pkt.TransportHeader.TCP == nil {
200+
pkt.TransportHeader.TCP = new(v1alpha1.TCPHeader)
201+
}
202+
pkt.TransportHeader.TCP.DstPort = ptr.To(int32(r))
203+
}
204+
if r, ok := fields["udp_src"]; ok {
205+
pkt.TransportHeader.UDP = new(v1alpha1.UDPHeader)
206+
pkt.TransportHeader.UDP.SrcPort = ptr.To(int32(r))
207+
}
208+
if r, ok := fields["udp_dst"]; ok {
209+
if pkt.TransportHeader.UDP != nil {
210+
pkt.TransportHeader.UDP = new(v1alpha1.UDPHeader)
211+
}
212+
pkt.TransportHeader.UDP.DstPort = ptr.To(int32(r))
213+
}
214+
return &pkt, nil
215+
}
216+
217+
func newPacketCapture() (*v1alpha1.PacketCapture, error) {
218+
var src v1alpha1.Source
219+
if option.source != "" {
220+
src.Pod, src.IP = parseEndpoint(option.source)
221+
if src.Pod == nil && src.IP == nil {
222+
return nil, fmt.Errorf("source should be in the format of Namespace/Pod, Pod, or IPv4")
223+
}
224+
}
225+
226+
var dst v1alpha1.Destination
227+
if option.dest != "" {
228+
dst.Pod, dst.IP = parseEndpoint(option.dest)
229+
if dst.Pod == nil && dst.IP == nil {
230+
return nil, fmt.Errorf("destination should be in the format of Namespace/Pod, Pod, or IPv4")
231+
}
232+
}
233+
234+
if src.Pod == nil && dst.Pod == nil {
235+
return nil, errors.New("one of source and destination must be a Pod")
236+
}
237+
pkt, err := parseFlow()
238+
if err != nil {
239+
return nil, fmt.Errorf("failed to parse flow: %w", err)
240+
}
241+
242+
name := getPCName(option.source, option.dest)
243+
pc := &v1alpha1.PacketCapture{
244+
ObjectMeta: metav1.ObjectMeta{
245+
Name: name,
246+
},
247+
Spec: v1alpha1.PacketCaptureSpec{
248+
Source: src,
249+
Destination: dst,
250+
Packet: pkt,
251+
CaptureConfig: v1alpha1.CaptureConfig{
252+
FirstN: &v1alpha1.PacketCaptureFirstNConfig{
253+
Number: option.number,
254+
},
255+
},
256+
},
257+
}
258+
return pc, nil
259+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package packetcapture

0 commit comments

Comments
 (0)