Skip to content

Commit bf4cb68

Browse files
committed
Support antctl command for packetcapture
Signed-off-by: Hang Yan <[email protected]>
1 parent 357d38f commit bf4cb68

File tree

6 files changed

+703
-1
lines changed

6 files changed

+703
-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,
+325
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
// Copyright 2025 Antrea Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package packetcapture
16+
17+
import (
18+
"context"
19+
"errors"
20+
"fmt"
21+
"net"
22+
"path/filepath"
23+
"strconv"
24+
"strings"
25+
"time"
26+
27+
"github.com/spf13/afero"
28+
"github.com/spf13/cobra"
29+
v1 "k8s.io/api/core/v1"
30+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31+
"k8s.io/apimachinery/pkg/util/intstr"
32+
"k8s.io/apimachinery/pkg/util/rand"
33+
"k8s.io/apimachinery/pkg/util/wait"
34+
"k8s.io/client-go/kubernetes"
35+
"k8s.io/client-go/rest"
36+
"k8s.io/utils/ptr"
37+
38+
"antrea.io/antrea/pkg/antctl/raw"
39+
"antrea.io/antrea/pkg/apis/crd/v1alpha1"
40+
antrea "antrea.io/antrea/pkg/client/clientset/versioned"
41+
"antrea.io/antrea/pkg/util/env"
42+
)
43+
44+
var (
45+
defaultTimeout = time.Second * 60
46+
Command *cobra.Command
47+
getClients = getConfigAndClients
48+
getCopier = getPodFile
49+
defaultFS = afero.NewOsFs()
50+
)
51+
var option = &struct {
52+
source string
53+
dest string
54+
nowait bool
55+
timeout time.Duration
56+
number int32
57+
flow string
58+
outputDir string
59+
}{}
60+
61+
var packetCaptureExample = strings.TrimSpace(`
62+
Start capture packets from pod1 to pod2, both Pods are in Namespace default
63+
$ antctl packetcaputre -S pod1 -D pod2
64+
Start capture packets from pod1 in Namespace ns1 to a destination IP
65+
$ antctl packetcapture -S ns1/pod1 -D 192.168.123.123
66+
Start capture UDP packets from pod1 to pod2, with destination port 1234
67+
$ antctl packetcapture -S pod1 -D pod2 -f udp,udp_dst=1234
68+
Save the packets file to a specified directory
69+
$ antctl packetcapture -S 192.168.123.123 -D pod2 -f tcp,tcp_dst=80 -o /tmp
70+
`)
71+
72+
func init() {
73+
Command = &cobra.Command{
74+
Use: "packetcapture",
75+
Short: "Start capture packets",
76+
Long: "Start capture packets on the target flow.",
77+
Aliases: []string{"pc", "packetcaptures"},
78+
Example: packetCaptureExample,
79+
RunE: packetCaptureRunE,
80+
}
81+
82+
Command.Flags().StringVarP(&option.source, "source", "S", "", "source of the the PacketCapture: Namespace/Pod, Pod, or IP")
83+
Command.Flags().StringVarP(&option.dest, "destination", "D", "", "destination of the PacketCapture: Namespace/Pod, Pod, or IP")
84+
Command.Flags().Int32VarP(&option.number, "number", "n", 0, "target packets number")
85+
Command.Flags().StringVarP(&option.flow, "flow", "f", "", "specify the flow (packet headers) of the PacketCapture , including tcp_src, tcp_dst, udp_src, udp_dst")
86+
Command.Flags().BoolVarP(&option.nowait, "nowait", "", false, "if set, command returns without retrieving results")
87+
Command.Flags().StringVarP(&option.outputDir, "output-dir", "o", ".", "save the packets file to the target directory")
88+
}
89+
90+
var protocols = map[string]int32{
91+
"icmp": 1,
92+
"tcp": 6,
93+
"udp": 17,
94+
}
95+
96+
func getFlowFields(flow string) (map[string]int, error) {
97+
fields := map[string]int{}
98+
for _, v := range strings.Split(flow, ",") {
99+
kv := strings.Split(v, "=")
100+
if len(kv) == 2 && len(kv[0]) != 0 && len(kv[1]) != 0 {
101+
r, err := strconv.Atoi(kv[1])
102+
if err != nil {
103+
return nil, err
104+
}
105+
fields[kv[0]] = r
106+
} else if len(kv) == 1 {
107+
if len(kv[0]) != 0 {
108+
fields[v] = 0
109+
}
110+
} else {
111+
return nil, fmt.Errorf("%s is not valid in flow", v)
112+
}
113+
}
114+
return fields, nil
115+
}
116+
117+
func getConfigAndClients(cmd *cobra.Command) (*rest.Config, kubernetes.Interface, antrea.Interface, error) {
118+
kubeConfig, err := raw.ResolveKubeconfig(cmd)
119+
if err != nil {
120+
return nil, nil, nil, err
121+
}
122+
k8sClientset, client, err := raw.SetupClients(kubeConfig)
123+
if err != nil {
124+
return nil, nil, nil, fmt.Errorf("failed to create clientset: %w", err)
125+
}
126+
return kubeConfig, k8sClientset, client, nil
127+
}
128+
129+
func getPodFile(cmd *cobra.Command) (PodFileCopy, error) {
130+
config, client, _, err := getClients(cmd)
131+
if err != nil {
132+
return nil, err
133+
}
134+
return &podFile{
135+
restConfig: config,
136+
restInterface: client.CoreV1().RESTClient(),
137+
}, nil
138+
}
139+
140+
func packetCaptureRunE(cmd *cobra.Command, args []string) error {
141+
option.timeout, _ = cmd.Flags().GetDuration("timeout")
142+
if option.timeout > time.Hour {
143+
return errors.New("timeout cannot be longer than 1 hour")
144+
}
145+
if option.timeout == 0 {
146+
option.timeout = defaultTimeout
147+
}
148+
if option.number == 0 {
149+
return errors.New("packet number should be larger than 0")
150+
}
151+
152+
_, _, antreaClient, err := getClients(cmd)
153+
if err != nil {
154+
return err
155+
}
156+
pc, err := newPacketCapture()
157+
if err != nil {
158+
return fmt.Errorf("error when filling up PacketCapture config: %w", err)
159+
}
160+
createCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
161+
defer cancel()
162+
163+
if _, err := antreaClient.CrdV1alpha1().PacketCaptures().Create(createCtx, pc, metav1.CreateOptions{}); err != nil {
164+
return fmt.Errorf("error when creating PacketCapture, is PacketCapture feature gate enabled? %w", err)
165+
}
166+
defer func() {
167+
if !option.nowait {
168+
if err = antreaClient.CrdV1alpha1().PacketCaptures().Delete(context.TODO(), pc.Name, metav1.DeleteOptions{}); err != nil {
169+
fmt.Fprintf(cmd.OutOrStdout(), "error when deleting PacketCapture: %s", err.Error())
170+
}
171+
}
172+
}()
173+
174+
if option.nowait {
175+
fmt.Fprintf(cmd.OutOrStdout(), "PacketCapture Name: %s\n", pc.Name)
176+
return nil
177+
}
178+
179+
var latestPC *v1alpha1.PacketCapture
180+
err = wait.PollUntilContextTimeout(context.TODO(), 1*time.Second, option.timeout, false, func(ctx context.Context) (bool, error) {
181+
res, err := antreaClient.CrdV1alpha1().PacketCaptures().Get(ctx, pc.Name, metav1.GetOptions{})
182+
if err != nil {
183+
return false, err
184+
}
185+
for _, cond := range res.Status.Conditions {
186+
if cond.Type == v1alpha1.PacketCaptureComplete && cond.Status == metav1.ConditionTrue {
187+
latestPC = res
188+
return true, nil
189+
}
190+
}
191+
return false, nil
192+
193+
})
194+
195+
if wait.Interrupted(err) {
196+
err = errors.New("timeout waiting for PacketCapture done")
197+
if latestPC == nil {
198+
return err
199+
}
200+
} else if err != nil {
201+
return fmt.Errorf("error when retrieving PacketCapture: %w", err)
202+
}
203+
204+
splits := strings.Split(latestPC.Status.FilePath, ":")
205+
fileName := filepath.Base(splits[1])
206+
copier, _ := getCopier(cmd)
207+
err = copier.CopyFromPod(context.TODO(), env.GetAntreaNamespace(), splits[0], "antrea-agent", splits[1], option.outputDir)
208+
if err == nil {
209+
fmt.Fprintf(cmd.OutOrStdout(), "Packet File: %s\n", filepath.Join(option.outputDir, fileName))
210+
}
211+
return err
212+
}
213+
214+
func parseEndpoint(endpoint string) (pod *v1alpha1.PodReference, ip *string) {
215+
parsedIP := net.ParseIP(endpoint)
216+
if parsedIP != nil && parsedIP.To4() != nil {
217+
ip = ptr.To(parsedIP.String())
218+
} else {
219+
split := strings.Split(endpoint, "/")
220+
if len(split) == 1 {
221+
pod = &v1alpha1.PodReference{
222+
Namespace: "default",
223+
Name: split[0],
224+
}
225+
} else if len(split) == 2 && len(split[0]) != 0 && len(split[1]) != 0 {
226+
pod = &v1alpha1.PodReference{
227+
Namespace: split[0],
228+
Name: split[1],
229+
}
230+
}
231+
}
232+
return
233+
}
234+
235+
func getPCName(src, dest string) string {
236+
replace := func(s string) string {
237+
return strings.ReplaceAll(s, "/", "-")
238+
}
239+
prefix := fmt.Sprintf("%s-%s", replace(src), replace(dest))
240+
if option.nowait {
241+
return prefix
242+
}
243+
return fmt.Sprintf("%s-%s", prefix, rand.String(8))
244+
}
245+
246+
func parseFlow() (*v1alpha1.Packet, error) {
247+
cleanFlow := strings.ReplaceAll(option.flow, " ", "")
248+
fields, err := getFlowFields(cleanFlow)
249+
if err != nil {
250+
return nil, fmt.Errorf("error when parsing the flow: %w", err)
251+
}
252+
var pkt v1alpha1.Packet
253+
pkt.IPFamily = v1.IPv4Protocol
254+
for k, v := range protocols {
255+
if _, ok := fields[k]; ok {
256+
pkt.Protocol = ptr.To(intstr.FromInt32(v))
257+
break
258+
}
259+
}
260+
if r, ok := fields["tcp_src"]; ok {
261+
pkt.TransportHeader.TCP = new(v1alpha1.TCPHeader)
262+
pkt.TransportHeader.TCP.SrcPort = ptr.To(int32(r))
263+
}
264+
if r, ok := fields["tcp_dst"]; ok {
265+
if pkt.TransportHeader.TCP == nil {
266+
pkt.TransportHeader.TCP = new(v1alpha1.TCPHeader)
267+
}
268+
pkt.TransportHeader.TCP.DstPort = ptr.To(int32(r))
269+
}
270+
if r, ok := fields["udp_src"]; ok {
271+
pkt.TransportHeader.UDP = new(v1alpha1.UDPHeader)
272+
pkt.TransportHeader.UDP.SrcPort = ptr.To(int32(r))
273+
}
274+
if r, ok := fields["udp_dst"]; ok {
275+
if pkt.TransportHeader.UDP != nil {
276+
pkt.TransportHeader.UDP = new(v1alpha1.UDPHeader)
277+
}
278+
pkt.TransportHeader.UDP.DstPort = ptr.To(int32(r))
279+
}
280+
return &pkt, nil
281+
}
282+
283+
func newPacketCapture() (*v1alpha1.PacketCapture, error) {
284+
var src v1alpha1.Source
285+
if option.source != "" {
286+
src.Pod, src.IP = parseEndpoint(option.source)
287+
if src.Pod == nil && src.IP == nil {
288+
return nil, fmt.Errorf("source should be in the format of Namespace/Pod, Pod, or IPv4")
289+
}
290+
}
291+
292+
var dst v1alpha1.Destination
293+
if option.dest != "" {
294+
dst.Pod, dst.IP = parseEndpoint(option.dest)
295+
if dst.Pod == nil && dst.IP == nil {
296+
return nil, fmt.Errorf("destination should be in the format of Namespace/Pod, Pod, or IPv4")
297+
}
298+
}
299+
300+
if src.Pod == nil && dst.Pod == nil {
301+
return nil, errors.New("one of source and destination must be a Pod")
302+
}
303+
pkt, err := parseFlow()
304+
if err != nil {
305+
return nil, fmt.Errorf("failed to parse flow: %w", err)
306+
}
307+
308+
name := getPCName(option.source, option.dest)
309+
pc := &v1alpha1.PacketCapture{
310+
ObjectMeta: metav1.ObjectMeta{
311+
Name: name,
312+
},
313+
Spec: v1alpha1.PacketCaptureSpec{
314+
Source: src,
315+
Destination: dst,
316+
Packet: pkt,
317+
CaptureConfig: v1alpha1.CaptureConfig{
318+
FirstN: &v1alpha1.PacketCaptureFirstNConfig{
319+
Number: option.number,
320+
},
321+
},
322+
},
323+
}
324+
return pc, nil
325+
}

0 commit comments

Comments
 (0)