Skip to content

Commit b34174d

Browse files
committed
KVM: packet capture of Instance NICs via operator-defined script
Adds root-admin-only APIs to capture the traffic of an Instance NIC on the KVM host it is running on, for debugging and lawful interception: * enablePacketCapture nicid=<uuid> * disablePacketCapture nicid=<uuid> * getPacketCaptureStatus nicid=<uuid> Packet capture is a flag on the NIC (stored as a nic detail). When enabled, the agent resolves the host-side tap device by MAC address, writes the NIC context (VM name/UUID, NIC UUID/MAC, IPv4/IPv6, bridge, network UUID) to /run/cloudstack/pcap-<dev>.env and starts the systemd template unit cloudstack-pcap@<dev>.service. The flag follows the Instance: a VM state listener restarts the capture after start and migration, and BindsTo= on the tap device stops the unit when the VM stops, migrates away or the NIC is unplugged. The unit executes pcap-capture.sh, shipped between the other KVM scripts in /usr/share/cloudstack-common as an example that runs tcpdump writing to /tmp with 256 MB rotation. What to capture and what to do with the data differs too much per environment for CloudStack to decide it, so operators run their own script by copying the unit, pointing its ExecStart at it and naming their unit in packet.capture.service. New agent.properties keys: packet.capture.service (systemd template unit name) and packet.capture.env.dir (environment file directory).
1 parent 6358dc3 commit b34174d

18 files changed

Lines changed: 1222 additions & 0 deletions

File tree

‎agent/src/main/java/com/cloud/agent/properties/AgentProperties.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,27 @@ public class AgentProperties{
660660
*/
661661
public static final Property<String> KVM_SCRIPTS_DIR = new Property<>("kvm.scripts.dir", "scripts/vm/hypervisor/kvm");
662662

663+
/**
664+
* Name of the systemd template unit used for packet capture of VM NICs.<br>
665+
* The agent starts and stops one instance of this unit per captured NIC, named
666+
* <code>&lt;unit&gt;@&lt;interface&gt;.service</code> (e.g. <code>cloudstack-pcap@vnet3.service</code>).<br>
667+
* When pointing this to a custom unit, make sure the unit reads its environment from
668+
* the file defined by {@link #PACKET_CAPTURE_ENV_DIR}.<br>
669+
* Data type: String.<br>
670+
* Default value: <code>cloudstack-pcap</code>
671+
*/
672+
public static final Property<String> PACKET_CAPTURE_SERVICE = new Property<>("packet.capture.service", "cloudstack-pcap");
673+
674+
/**
675+
* Directory in which the agent writes the environment file for a packet capture,
676+
* named <code>pcap-&lt;interface&gt;.env</code>. The default matches the
677+
* <code>EnvironmentFile=</code> of the shipped cloudstack-pcap@.service unit;
678+
* change both together.<br>
679+
* Data type: String.<br>
680+
* Default value: <code>/run/cloudstack</code>
681+
*/
682+
public static final Property<String> PACKET_CAPTURE_ENV_DIR = new Property<>("packet.capture.env.dir", "/run/cloudstack");
683+
663684
/**
664685
* Specifies start MAC address for private IP range.<br>
665686
* Data type: String.<br>

‎api/src/main/java/com/cloud/event/EventTypes.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ public class EventTypes {
238238
public static final String EVENT_NIC_PRIMARY_NETWORK_CHANGE = "NIC.PRIMARY.NETWORK.CHANGE";
239239
// internal-only signal consumed by the usage job - never user-facing, not an audit event
240240
public static final String EVENT_NIC_NETWORK_ID_BACKFILL = "NIC.NETWORK.ID.BACKFILL";
241+
public static final String EVENT_NIC_PACKET_CAPTURE_ENABLE = "NIC.PACKETCAPTURE.ENABLE";
242+
public static final String EVENT_NIC_PACKET_CAPTURE_DISABLE = "NIC.PACKETCAPTURE.DISABLE";
241243

242244
// Load Balancers
243245
public static final String EVENT_ASSIGN_TO_LOAD_BALANCER_RULE = "LB.ASSIGN.TO.RULE";
@@ -1008,6 +1010,8 @@ public class EventTypes {
10081010
entityEventDetails.put(EVENT_NIC_NETWORK_ASSOCIATE, Nic.class);
10091011
entityEventDetails.put(EVENT_NIC_NETWORK_DISASSOCIATE, Nic.class);
10101012
entityEventDetails.put(EVENT_NIC_PRIMARY_NETWORK_CHANGE, Nic.class);
1013+
entityEventDetails.put(EVENT_NIC_PACKET_CAPTURE_ENABLE, Nic.class);
1014+
entityEventDetails.put(EVENT_NIC_PACKET_CAPTURE_DISABLE, Nic.class);
10111015

10121016
// Load Balancers
10131017
entityEventDetails.put(EVENT_ASSIGN_TO_LOAD_BALANCER_RULE, FirewallRule.class);
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.api.command.admin.nic;
18+
19+
import javax.inject.Inject;
20+
21+
import org.apache.cloudstack.acl.RoleType;
22+
import org.apache.cloudstack.api.APICommand;
23+
import org.apache.cloudstack.api.ApiConstants;
24+
import org.apache.cloudstack.api.ApiErrorCode;
25+
import org.apache.cloudstack.api.BaseAsyncCmd;
26+
import org.apache.cloudstack.api.Parameter;
27+
import org.apache.cloudstack.api.ServerApiException;
28+
import org.apache.cloudstack.api.response.NicResponse;
29+
import org.apache.cloudstack.api.response.SuccessResponse;
30+
import org.apache.cloudstack.context.CallContext;
31+
import org.apache.cloudstack.network.packetcapture.PacketCaptureService;
32+
33+
import com.cloud.event.EventTypes;
34+
import com.cloud.exception.ResourceUnavailableException;
35+
import com.cloud.utils.exception.CloudRuntimeException;
36+
import com.cloud.vm.Nic;
37+
38+
@APICommand(name = "disablePacketCapture", responseObject = SuccessResponse.class, entityType = {Nic.class},
39+
responseHasSensitiveInfo = false,
40+
requestHasSensitiveInfo = false,
41+
description = "Disables packet capture on an Instance NIC and stops a running capture. Only supported on KVM.",
42+
authorized = {RoleType.Admin},
43+
since = "4.23.0")
44+
public class DisablePacketCaptureCmd extends BaseAsyncCmd {
45+
46+
@Inject
47+
private PacketCaptureService packetCaptureService;
48+
49+
/////////////////////////////////////////////////////
50+
//////////////// API parameters /////////////////////
51+
/////////////////////////////////////////////////////
52+
@Parameter(name = ApiConstants.NIC_ID, type = CommandType.UUID, entityType = NicResponse.class, required = true,
53+
description = "The ID of the NIC to stop capturing packets on")
54+
private Long nicId;
55+
56+
/////////////////////////////////////////////////////
57+
/////////////////// Accessors ///////////////////////
58+
/////////////////////////////////////////////////////
59+
public Long getNicId() {
60+
return nicId;
61+
}
62+
63+
/////////////////////////////////////////////////////
64+
/////////////////// Implementation //////////////////
65+
/////////////////////////////////////////////////////
66+
@Override
67+
public long getEntityOwnerId() {
68+
return CallContext.current().getCallingAccountId();
69+
}
70+
71+
@Override
72+
public String getEventType() {
73+
return EventTypes.EVENT_NIC_PACKET_CAPTURE_DISABLE;
74+
}
75+
76+
@Override
77+
public String getEventDescription() {
78+
return "Disabling packet capture on NIC with ID: " + getResourceUuid(ApiConstants.NIC_ID);
79+
}
80+
81+
@Override
82+
public void execute() throws ResourceUnavailableException, ServerApiException {
83+
try {
84+
packetCaptureService.disablePacketCapture(getNicId());
85+
SuccessResponse response = new SuccessResponse(getCommandName());
86+
setResponseObject(response);
87+
} catch (CloudRuntimeException e) {
88+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
89+
}
90+
}
91+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.api.command.admin.nic;
18+
19+
import javax.inject.Inject;
20+
21+
import org.apache.cloudstack.acl.RoleType;
22+
import org.apache.cloudstack.api.APICommand;
23+
import org.apache.cloudstack.api.ApiConstants;
24+
import org.apache.cloudstack.api.ApiErrorCode;
25+
import org.apache.cloudstack.api.BaseAsyncCmd;
26+
import org.apache.cloudstack.api.Parameter;
27+
import org.apache.cloudstack.api.ServerApiException;
28+
import org.apache.cloudstack.api.response.NicResponse;
29+
import org.apache.cloudstack.api.response.SuccessResponse;
30+
import org.apache.cloudstack.context.CallContext;
31+
import org.apache.cloudstack.network.packetcapture.PacketCaptureService;
32+
33+
import com.cloud.event.EventTypes;
34+
import com.cloud.exception.ResourceUnavailableException;
35+
import com.cloud.utils.exception.CloudRuntimeException;
36+
import com.cloud.vm.Nic;
37+
38+
@APICommand(name = "enablePacketCapture", responseObject = SuccessResponse.class, entityType = {Nic.class},
39+
responseHasSensitiveInfo = false,
40+
requestHasSensitiveInfo = false,
41+
description = "Enables packet capture on an Instance NIC. The capture is started on the host the Instance " +
42+
"is running on and follows the Instance across stop/start and migration. Only supported on KVM.",
43+
authorized = {RoleType.Admin},
44+
since = "4.23.0")
45+
public class EnablePacketCaptureCmd extends BaseAsyncCmd {
46+
47+
@Inject
48+
private PacketCaptureService packetCaptureService;
49+
50+
/////////////////////////////////////////////////////
51+
//////////////// API parameters /////////////////////
52+
/////////////////////////////////////////////////////
53+
@Parameter(name = ApiConstants.NIC_ID, type = CommandType.UUID, entityType = NicResponse.class, required = true,
54+
description = "The ID of the NIC to capture packets on")
55+
private Long nicId;
56+
57+
/////////////////////////////////////////////////////
58+
/////////////////// Accessors ///////////////////////
59+
/////////////////////////////////////////////////////
60+
public Long getNicId() {
61+
return nicId;
62+
}
63+
64+
/////////////////////////////////////////////////////
65+
/////////////////// Implementation //////////////////
66+
/////////////////////////////////////////////////////
67+
@Override
68+
public long getEntityOwnerId() {
69+
return CallContext.current().getCallingAccountId();
70+
}
71+
72+
@Override
73+
public String getEventType() {
74+
return EventTypes.EVENT_NIC_PACKET_CAPTURE_ENABLE;
75+
}
76+
77+
@Override
78+
public String getEventDescription() {
79+
return "Enabling packet capture on NIC with ID: " + getResourceUuid(ApiConstants.NIC_ID);
80+
}
81+
82+
@Override
83+
public void execute() throws ResourceUnavailableException, ServerApiException {
84+
try {
85+
packetCaptureService.enablePacketCapture(getNicId());
86+
SuccessResponse response = new SuccessResponse(getCommandName());
87+
setResponseObject(response);
88+
} catch (CloudRuntimeException e) {
89+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
90+
}
91+
}
92+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.api.command.admin.nic;
18+
19+
import javax.inject.Inject;
20+
21+
import org.apache.cloudstack.acl.RoleType;
22+
import org.apache.cloudstack.api.APICommand;
23+
import org.apache.cloudstack.api.ApiConstants;
24+
import org.apache.cloudstack.api.ApiErrorCode;
25+
import org.apache.cloudstack.api.BaseCmd;
26+
import org.apache.cloudstack.api.Parameter;
27+
import org.apache.cloudstack.api.ServerApiException;
28+
import org.apache.cloudstack.api.response.NicResponse;
29+
import org.apache.cloudstack.api.response.PacketCaptureResponse;
30+
import org.apache.cloudstack.context.CallContext;
31+
import org.apache.cloudstack.network.packetcapture.PacketCaptureService;
32+
33+
import com.cloud.utils.exception.CloudRuntimeException;
34+
import com.cloud.vm.Nic;
35+
36+
@APICommand(name = "getPacketCaptureStatus", responseObject = PacketCaptureResponse.class, entityType = {Nic.class},
37+
responseHasSensitiveInfo = false,
38+
requestHasSensitiveInfo = false,
39+
description = "Returns whether packet capture is enabled on an Instance NIC and whether a capture is " +
40+
"currently running on the host. Only supported on KVM.",
41+
authorized = {RoleType.Admin},
42+
since = "4.23.0")
43+
public class GetPacketCaptureStatusCmd extends BaseCmd {
44+
45+
@Inject
46+
private PacketCaptureService packetCaptureService;
47+
48+
/////////////////////////////////////////////////////
49+
//////////////// API parameters /////////////////////
50+
/////////////////////////////////////////////////////
51+
@Parameter(name = ApiConstants.NIC_ID, type = CommandType.UUID, entityType = NicResponse.class, required = true,
52+
description = "The ID of the NIC to get the packet capture status of")
53+
private Long nicId;
54+
55+
/////////////////////////////////////////////////////
56+
/////////////////// Accessors ///////////////////////
57+
/////////////////////////////////////////////////////
58+
public Long getNicId() {
59+
return nicId;
60+
}
61+
62+
/////////////////////////////////////////////////////
63+
/////////////////// Implementation //////////////////
64+
/////////////////////////////////////////////////////
65+
@Override
66+
public long getEntityOwnerId() {
67+
return CallContext.current().getCallingAccountId();
68+
}
69+
70+
@Override
71+
public void execute() throws ServerApiException {
72+
try {
73+
PacketCaptureResponse response = packetCaptureService.getPacketCaptureStatus(getNicId());
74+
response.setObjectName("packetcapture");
75+
response.setResponseName(getCommandName());
76+
setResponseObject(response);
77+
} catch (CloudRuntimeException e) {
78+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, e.getMessage());
79+
}
80+
}
81+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.api.response;
18+
19+
import org.apache.cloudstack.api.ApiConstants;
20+
import org.apache.cloudstack.api.BaseResponse;
21+
22+
import com.cloud.serializer.Param;
23+
import com.google.gson.annotations.SerializedName;
24+
25+
public class PacketCaptureResponse extends BaseResponse {
26+
27+
@SerializedName(ApiConstants.NIC_ID)
28+
@Param(description = "the ID of the NIC")
29+
private String nicId;
30+
31+
@SerializedName(ApiConstants.VIRTUAL_MACHINE_ID)
32+
@Param(description = "the ID of the Instance the NIC belongs to")
33+
private String virtualMachineId;
34+
35+
@SerializedName(ApiConstants.VIRTUAL_MACHINE_NAME)
36+
@Param(description = "the internal name of the Instance the NIC belongs to")
37+
private String virtualMachineName;
38+
39+
@SerializedName(ApiConstants.MAC_ADDRESS)
40+
@Param(description = "the MAC address of the NIC")
41+
private String macAddress;
42+
43+
@SerializedName(ApiConstants.ENABLED)
44+
@Param(description = "true if packet capture is enabled on the NIC")
45+
private Boolean enabled;
46+
47+
@SerializedName("running")
48+
@Param(description = "true if a packet capture is currently running on the host for the NIC")
49+
private Boolean running;
50+
51+
public void setNicId(String nicId) {
52+
this.nicId = nicId;
53+
}
54+
55+
public void setVirtualMachineId(String virtualMachineId) {
56+
this.virtualMachineId = virtualMachineId;
57+
}
58+
59+
public void setVirtualMachineName(String virtualMachineName) {
60+
this.virtualMachineName = virtualMachineName;
61+
}
62+
63+
public void setMacAddress(String macAddress) {
64+
this.macAddress = macAddress;
65+
}
66+
67+
public void setEnabled(Boolean enabled) {
68+
this.enabled = enabled;
69+
}
70+
71+
public void setRunning(Boolean running) {
72+
this.running = running;
73+
}
74+
75+
public Boolean getEnabled() {
76+
return enabled;
77+
}
78+
79+
public Boolean getRunning() {
80+
return running;
81+
}
82+
}

0 commit comments

Comments
 (0)