Setting the file. One moment. Create Capture · AKS Network Capture · microsoft/azure-skills · Skills DocsScript Collect Azure Network Info
scripts/create-capture.ps1
PowerShell·326 lines·12 KB
string
]$PodSelector
,
10 [string]$PodNames,
11 [string]$Namespace = "default",
12 [string]$TcpdumpFilter = "",
13 [string]$PacketSize = "0",
14 [switch]$Help
15)
16
17$CaptureImage = "mcr.microsoft.com/containernetworking/retina-shell:v1.2.3@sha256:c7dfe8e0c0dc7fa28e4cfbad04ade270c3051c42a5495488d4d897b49fb3366f"
18$ConfigMapName = "network-capture-scripts"
19$OutputBase = "/var/log/aks-network-captures"
20$CaptureMountPath = "/capture-output"
21$MaxDurationSeconds = 1800
22
23function Write-Usage {
24 @"
25Usage: create-capture.ps1 -Name <capture-name> [options]
26
27Target options:
28 -NodeSelector <key=val> Node selector (default: kubernetes.io/os=linux)
29 -NodeNames <n1,n2> Comma-separated node names
30 -PodSelector <key=val> Pod selector; resolves pods to nodes and IPs
31 -PodNames <p1,p2> Comma-separated pod names
32 -Namespace <name> Pod, ConfigMap, and Job namespace
33
34Capture options:
35 -Duration <Ns|Nm|Nh> Duration (default: 60s; maximum: 1800s)
36 -PacketSize <bytes> Snapshot length (default: 0, full packet)
37 -TcpdumpFilter <expr> Plain BPF expression; no flags or metacharacters
38"@
39}
40
41function Stop-Usage {
42 param([string]$Message)
43 [Console]::Error.WriteLine("Error: $Message")
44 [Console]::Error.WriteLine((Write-Usage))
45 exit 2
46}
47
48function Stop-Failure {
49 param([string]$Message)
50 [Console]::Error.WriteLine("Error: $Message")
51 exit 1
52}
53
54function Test-Rfc1123Label {
55 param([string]$Value)
56 return $Value.Length -le 63 -and
57 $Value -cmatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
58}
59
60function Test-DnsSubdomain {
61 param([string]$Value)
62 return $Value.Length -le 253 -and
63 $Value -cmatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
64}
65
66function Invoke-KubectlText {
67 param([string[]]$KubectlArguments, [string]$FailureMessage)
68 $output = (& kubectl @KubectlArguments 2>&1) -join "`n"
69 if ($LASTEXITCODE -ne 0) {
70 Stop-Failure "$FailureMessage`n$output"
71 }
72 return $output.Trim()
73}
74
75if ($Help) {
76 Write-Usage
77 exit 0
78}
79if ([string]::IsNullOrWhiteSpace($Name)) {
80 Stop-Usage "-Name is required"
81}
82if (-not (Test-Rfc1123Label $Name)) {
83 Stop-Usage "-Name must be an RFC 1123 label"
84}
85if (-not (Test-Rfc1123Label $Namespace)) {
86 Stop-Usage "-Namespace must be an RFC 1123 label"
87}
88if ($Duration -cnotmatch "^([0-9]+)([smh])$") {
89 Stop-Usage "-Duration must look like 30s, 5m, or 1h"
90}
91$durationNumber = [int64]$Matches[1]
92$durationSeconds = switch ($Matches[2]) {
93 "s" { $durationNumber }
94 "m" { $durationNumber * 60 }
95 "h" { $durationNumber * 3600 }
96}
97if ($durationSeconds -lt 1 -or $durationSeconds -gt $MaxDurationSeconds) {
98 Stop-Usage "-Duration must be between 1s and ${MaxDurationSeconds}s"
99}
100if ($PacketSize -notmatch "^[0-9]+$") {
101 Stop-Usage "-PacketSize must be a non-negative integer"
102}
103$packetSizeValue = [int64]$PacketSize
104if ($packetSizeValue -gt 262144) {
105 Stop-Usage "-PacketSize exceeds 262144 bytes"
106}
107if ($NodeSelector -notmatch "^[A-Za-z0-9._/=,!()-]+$") {
108 Stop-Usage "-NodeSelector has invalid characters"
109}
110if ($PodSelector -and $PodSelector -notmatch "^[A-Za-z0-9._/=,!()-]+$") {
111 Stop-Usage "-PodSelector has invalid characters"
112}
113if ($TcpdumpFilter.Length -gt 1024 -or $TcpdumpFilter -match "[`r`n]") {
114 Stop-Usage "-TcpdumpFilter must be one line and no more than 1024 characters"
115}
116if ($TcpdumpFilter -and $TcpdumpFilter -notmatch "^[A-Za-z0-9 ._:/()&|<>=!-]+$") {
117 Stop-Usage "-TcpdumpFilter contains disallowed characters"
118}
119foreach ($token in ($TcpdumpFilter -split "\s+")) {
120 if ($token.StartsWith("-")) {
121 Stop-Usage "-TcpdumpFilter may not contain flag tokens"
122 }
123}
124
125$selectionCount = 0
126foreach ($selection in @($NodeNames, $PodSelector, $PodNames)) {
127 if (-not [string]::IsNullOrWhiteSpace($selection)) {
128 $selectionCount++
129 }
130}
131if ($selectionCount -gt 1) {
132 Stop-Usage "choose only one of -NodeNames, -PodSelector, or -PodNames"
133}
134if (-not (Get-Command kubectl -ErrorAction SilentlyContinue)) {
135 Stop-Failure "kubectl is not installed or not on PATH"
136}
137
138$deployedRunner = Invoke-KubectlText -KubectlArguments @(
139 "get", "configmap", $ConfigMapName, "-n", $Namespace,
140 "-o", "go-template={{ index .data `"run-capture.sh`" }}"
141) -FailureMessage "ConfigMap '$ConfigMapName' was not found; run setup-capture-configmap first"
142$runnerPath = Join-Path $PSScriptRoot "run-capture.sh"
143try {
144 $localRunner = Get-Content -LiteralPath $runnerPath -Raw -ErrorAction Stop
145} catch {
146 Stop-Failure "could not read the local capture runner: $($_.Exception.Message)"
147}
148if ($deployedRunner.TrimEnd("`r", "`n") -ne $localRunner.TrimEnd("`r", "`n")) {
149 Stop-Failure "ConfigMap '$ConfigMapName' is stale; run setup-capture-configmap again"
150}
151
152$targetNodes = New-Object System.Collections.Generic.List[string]
153$podIpTerms = New-Object System.Collections.Generic.List[string]
154$podJsonPath = '{range .items[*]}{.metadata.name}{"|"}{.spec.nodeName}{"|"}{range .status.podIPs[*]}{.ip}{","}{end}{"\n"}{end}'
155
156if ($PodNames -or $PodSelector) {
157 if ($PodNames) {
158 $pods = @($PodNames.Split(","))
159 foreach ($pod in $pods) {
160 if (-not (Test-Rfc1123Label $pod)) {
161 Stop-Usage "pod name '$pod' is not an RFC 1123 label"
162 }
163 }
164 $podOutput = Invoke-KubectlText -KubectlArguments (@(
165 "get", "pods", "-n", $Namespace
166 ) + $pods + @("-o", "jsonpath=$podJsonPath")) -FailureMessage "could not resolve the selected pods"
167 } else {
168 $podOutput = Invoke-KubectlText -KubectlArguments @(
169 "get", "pods", "-n", $Namespace, "-l", $PodSelector,
170 "-o", "jsonpath=$podJsonPath"
171 ) -FailureMessage "could not resolve the pod selector"
172 }
173
174 foreach ($record in @($podOutput -split "`n")) {
175 if ([string]::IsNullOrWhiteSpace($record)) {
176 continue
177 }
178 $parts = $record.TrimEnd("`r").Split("|")
179 if ($parts.Count -ne 3 -or [string]::IsNullOrWhiteSpace($parts[1])) {
180 Stop-Failure "Kubernetes returned an invalid pod placement record"
181 }
182 $targetNodes.Add($parts[1])
183 $ips = @($parts[2].Split(",") | Where-Object { $_ })
184 if ($ips.Count -eq 0) {
185 Stop-Failure "selected pod '$($parts[0])' has no assigned IP"
186 }
187 foreach ($ip in $ips) {
188 if ($ip -notmatch "^[A-Fa-f0-9.:]+$") {
189 Stop-Failure "selected pod '$($parts[0])' returned an invalid IP"
190 }
191 $podIpTerms.Add("host $ip")
192 }
193 }
194} elseif ($NodeNames) {
195 foreach ($node in $NodeNames.Split(",")) {
196 $targetNodes.Add($node)
197 }
198} else {
199 $nodeOutput = Invoke-KubectlText -KubectlArguments @(
200 "get", "nodes", "-l", $NodeSelector,
201 "-o", 'jsonpath={range .items[*]}{.metadata.name}{"\n"}{end}'
202 ) -FailureMessage "could not resolve the node selector"
203 foreach ($node in @($nodeOutput -split "`n")) {
204 if (-not [string]::IsNullOrWhiteSpace($node)) {
205 $targetNodes.Add($node.TrimEnd("`r"))
206 }
207 }
208}
209if ($targetNodes.Count -eq 0) {
210 Stop-Failure "no nodes matched the selection"
211}
212
213$uniqueNodes = New-Object System.Collections.Generic.List[string]
214foreach ($node in $targetNodes) {
215 if (-not (Test-DnsSubdomain $node)) {
216 Stop-Usage "node name '$node' is invalid"
217 }
218 if (-not $uniqueNodes.Contains($node)) {
219 $uniqueNodes.Add($node)
220 }
221}
222
223$effectiveFilter = $TcpdumpFilter
224if ($podIpTerms.Count -gt 0) {
225 $podFilter = "(" + ($podIpTerms -join " or ") + ")"
226 $effectiveFilter = if ($effectiveFilter) {
227 "($effectiveFilter) and $podFilter"
228 } else {
229 $podFilter
230 }
231}
232
233$randomBytes = New-Object byte[] 12
234$rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
235try {
236 $rng.GetBytes($randomBytes)
237} finally {
238 $rng.Dispose()
239}
240$runToken = -join ($randomBytes | ForEach-Object { $_.ToString("x2") })
241$timestamp = (Get-Date).ToUniversalTime().ToString("yyyyMMdd-HHmmss")
242$runId = "$timestamp-$runToken"
243$captureOutputPath = "$OutputBase/$Name/$runId"
244$createdJobs = New-Object System.Collections.Generic.List[string]
245
246function Remove-PartialJobs {
247 if ($createdJobs.Count -eq 0) {
248 return $true
249 }
250 & kubectl delete jobs -n $Namespace @($createdJobs) --ignore-not-found | Out-Null
251 return $LASTEXITCODE -eq 0
252}
253
254Write-Output "Creating capture '$Name' on $($uniqueNodes.Count) node(s)"
255foreach ($node in $uniqueNodes) {
256 $nodeSlug = (($node -replace "[._]", "-").Substring(0, [Math]::Min(40, $node.Length))).TrimEnd("-")
257 $captureSlug = $Name.Substring(0, [Math]::Min(15, $Name.Length)).TrimEnd("-")
258 $nodePrefix = $nodeSlug.Substring(0, [Math]::Min(8, $nodeSlug.Length)).TrimEnd("-")
259 $jobPrefix = "$captureSlug-$nodePrefix-$runToken-"
260 $jobYaml = @"
261apiVersion: batch/v1
262kind: Job
263metadata:
264 generateName: $jobPrefix
265 namespace: $Namespace
266 labels: { app: aks-network-capture, capture-id: "$Name", capture-node: "$nodeSlug", capture-run: "$runId" }
267spec:
268 ttlSecondsAfterFinished: 3600
269 backoffLimit: 0
270 template:
271 metadata:
272 labels: { app: aks-network-capture, capture-id: "$Name", capture-run: "$runId" }
273 spec:
274 hostNetwork: true
275 nodeName: "$node"
276 restartPolicy: Never
277 terminationGracePeriodSeconds: $MaxDurationSeconds
278 containers:
279 - name: capture
280 image: "$CaptureImage"
281 securityContext:
282 privileged: false
283 allowPrivilegeEscalation: false
284 readOnlyRootFilesystem: true
285 runAsUser: 0
286 capabilities:
287 drop: ["ALL"]
288 add: ["NET_ADMIN", "NET_RAW"]
289 env:
290 - { name: PCAP_FILTER, value: "$effectiveFilter" }
291 - { name: CAPTURE_DURATION, value: "$durationSeconds" }
292 - { name: PACKET_SIZE, value: "$packetSizeValue" }
293 - { name: OUT_DIR, value: "$CaptureMountPath" }
294 - { name: CAPTURE_ID, value: "$Name" }
295 - { name: RUN_ID, value: "$runId" }
296 - { name: NODE_NAME, valueFrom: { fieldRef: { fieldPath: spec.nodeName } } }
297 command: ["/bin/sh", "/capture-scripts/run-capture.sh"]
298 volumeMounts:
299 - { name: capture-output, mountPath: "$CaptureMountPath" }
300 - { name: capture-scripts, mountPath: /capture-scripts, readOnly: true }
301 volumes:
302 - name: capture-output
303 hostPath: { path: "$captureOutputPath", type: DirectoryOrCreate }
304 - name: capture-scripts
305 configMap:
306 name: $ConfigMapName
307 defaultMode: 0555
308"@
309 $createdJobOutput = ($jobYaml | & kubectl create -f - -o 'jsonpath={.metadata.name}' 2>&1) -join "`n"
310 if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($createdJobOutput)) {
311 $cleanupSucceeded = Remove-PartialJobs
312 if (-not $cleanupSucceeded) {
313 Stop-Failure "failed to create a Job for '$node' and failed to remove partial Jobs"
314 }
315 Stop-Failure "failed to create a capture Job for '$node': $createdJobOutput"
316 }
317 $createdJobs.Add($createdJobOutput.Trim())
318 Write-Output "Job created for node: $node ($($createdJobOutput.Trim()))"
319}
320
321Write-Output ""
322Write-Output "Capture started."
323Write-Output "Run ID: $runId"
324Write-Output "Monitor: kubectl get jobs -n $Namespace -l capture-id=$Name -w"
325Write-Output "Retrieve: ./scripts/retrieve-captures.ps1 -Name $Name -RunId $runId -Namespace $Namespace"
326Write-Output "Bundles: $captureOutputPath"