Network Tracing | ||
---|---|---|
Performance Tests | Markers |
Supporting a new network protocol in TMF is straightforward. Minimal effort is required to support new protocols. In this tutorial, the UDP protocol will be added to the list of supported protocols.
All the TMF pcap-related code is divided in three projects (not considering the tests plugins):
The UDP is a transport-layer protocol that does not guarantee message delivery nor in-order message reception. A UDP packet (datagram) has the following structure:
Offsets | Octet | 0 | 1 | 2 | 3 | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Octet | Bit | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
0 | 0 | Source port | Destination port | ||||||||||||||||||||||||||||||
4 | 32 | Length | Checksum |
Knowing that, we can define an UDPPacket class that contains those fields.
First, in org.eclipse.tracecompass.pcap.core, create a new package named org.eclipse.tracecompass.pcap.core.protocol.name with name being the name of the new protocol. In our case name is udp so we create the package org.eclipse.tracecompass.pcap.core.protocol.udp. All our work is going in this package.
In this package, we create a new class named UDPPacket that extends Packet. All new protocol must define a packet type that extends the abstract class Packet. We also add different fields:
We also create the UDPPacket(PcapFile file, @Nullable Packet parent, ByteBuffer packet) constructor. The parameters are:
The following class is obtained:
package org.eclipse.tracecompass.pcap.core.protocol.udp; import java.nio.ByteBuffer; import java.util.Map; import org.eclipse.tracecompass.internal.pcap.core.endpoint.ProtocolEndpoint; import org.eclipse.tracecompass.internal.pcap.core.packet.BadPacketException; import org.eclipse.tracecompass.internal.pcap.core.packet.Packet; public class UDPPacket extends Packet { private final @Nullable Packet fChildPacket; private final @Nullable ByteBuffer fPayload; private final int fSourcePort; private final int fDestinationPort; private final int fTotalLength; private final int fChecksum; private @Nullable UDPEndpoint fSourceEndpoint; private @Nullable UDPEndpoint fDestinationEndpoint; private @Nullable ImmutableMap<String, String> fFields; /** * Constructor of the UDP Packet class. * * @param file * The file that contains this packet. * @param parent * The parent packet of this packet (the encapsulating packet). * @param packet * The entire packet (header and payload). * @throws BadPacketException * Thrown when the packet is erroneous. */ public UDPPacket(PcapFile file, @Nullable Packet parent, ByteBuffer packet) throws BadPacketException { super(file, parent, PcapProtocol.UDP); // TODO Auto-generated constructor stub } @Override public Packet getChildPacket() { // TODO Auto-generated method stub return null; } @Override public ByteBuffer getPayload() { // TODO Auto-generated method stub return null; } @Override public boolean validate() { // TODO Auto-generated method stub return false; } @Override protected Packet findChildPacket() throws BadPacketException { // TODO Auto-generated method stub return null; } @Override public ProtocolEndpoint getSourceEndpoint() { // TODO Auto-generated method stub return null; } @Override public ProtocolEndpoint getDestinationEndpoint() { // TODO Auto-generated method stub return null; } @Override public Map<String, String> getFields() { // TODO Auto-generated method stub return null; } @Override public String getLocalSummaryString() { // TODO Auto-generated method stub return null; } @Override protected String getSignificationString() { // TODO Auto-generated method stub return null; } @Override public boolean equals(Object obj) { // TODO Auto-generated method stub return false; } @Override public int hashCode() { // TODO Auto-generated method stub return 0; } }
Now, we implement the constructor. It is done in four steps:
The following constructor is obtained:
public UDPPacket(PcapFile file, @Nullable Packet parent, ByteBuffer packet) throws BadPacketException { super(file, parent, Protocol.UDP); // The endpoints and fFields are lazy loaded. They are defined in the get*Endpoint() // methods. fSourceEndpoint = null; fDestinationEndpoint = null; fFields = null; // Initialize the fields from the ByteBuffer packet.order(ByteOrder.BIG_ENDIAN); packet.position(0); fSourcePort = ConversionHelper.unsignedShortToInt(packet.getShort()); fDestinationPort = ConversionHelper.unsignedShortToInt(packet.getShort()); fTotalLength = ConversionHelper.unsignedShortToInt(packet.getShort()); fChecksum = ConversionHelper.unsignedShortToInt(packet.getShort()); // Initialize the payload if (packet.array().length - packet.position() > 0) { byte[] array = new byte[packet.array().length - packet.position()]; packet.get(array); ByteBuffer payload = ByteBuffer.wrap(array); payload.order(ByteOrder.BIG_ENDIAN); payload.position(0); fPayload = payload; } else { fPayload = null; } // Find child fChildPacket = findChildPacket(); }
Then, we implement the following methods:
We get the following code:
@Override public @Nullable Packet getChildPacket() { return fChildPacket; } @Override public @Nullable ByteBuffer getPayload() { return fPayload; } /** * Getter method that returns the UDP Source Port. * * @return The source Port. */ public int getSourcePort() { return fSourcePort; } /** * Getter method that returns the UDP Destination Port. * * @return The destination Port. */ public int getDestinationPort() { return fDestinationPort; } /** * {@inheritDoc} * * See http://www.iana.org/assignments/service-names-port-numbers/service- * names-port-numbers.xhtml or * http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers */ @Override protected @Nullable Packet findChildPacket() throws BadPacketException { // When more protocols are implemented, we can simply do a switch on the fDestinationPort field to find the child packet. // For instance, if the destination port is 80, then chances are the HTTP protocol is encapsulated. We can create a new HTTP // packet (after some verification that it is indeed the HTTP protocol). ByteBuffer payload = fPayload; if (payload == null) { return null; } return new UnknownPacket(getPcapFile(), this, payload); } @Override public boolean validate() { // Not yet implemented. ATM, we consider that all packets are valid. // TODO Implement it. We can compute the real checksum and compare it to fChecksum. return true; } @Override public UDPEndpoint getSourceEndpoint() { @Nullable UDPEndpoint endpoint = fSourceEndpoint; if (endpoint == null) { endpoint = new UDPEndpoint(this, true); } fSourceEndpoint = endpoint; return fSourceEndpoint; } @Override public UDPEndpoint getDestinationEndpoint() { @Nullable UDPEndpoint endpoint = fDestinationEndpoint; if (endpoint == null) { endpoint = new UDPEndpoint(this, false); } fDestinationEndpoint = endpoint; return fDestinationEndpoint; } @Override public Map<String, String> getFields() { ImmutableMap<String, String> map = fFields; if (map == null) { @SuppressWarnings("null") @NonNull ImmutableMap<String, String> newMap = ImmutableMap.<String, String> builder() .put("Source Port", String.valueOf(fSourcePort)) //$NON-NLS-1$ .put("Destination Port", String.valueOf(fDestinationPort)) //$NON-NLS-1$ .put("Length", String.valueOf(fTotalLength) + " bytes") //$NON-NLS-1$ //$NON-NLS-2$ .put("Checksum", String.format("%s%04x", "0x", fChecksum)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ .build(); fFields = newMap; return newMap; } return map; } @Override public String getLocalSummaryString() { return "Src Port: " + fSourcePort + ", Dst Port: " + fDestinationPort; //$NON-NLS-1$ //$NON-NLS-2$ } @Override protected String getSignificationString() { return "Source Port: " + fSourcePort + ", Destination Port: " + fDestinationPort; //$NON-NLS-1$ //$NON-NLS-2$ } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + fChecksum; final Packet child = fChildPacket; if (child != null) { result = prime * result + child.hashCode(); } else { result = prime * result; } result = prime * result + fDestinationPort; final ByteBuffer payload = fPayload; if (payload != null) { result = prime * result + payload.hashCode(); } else { result = prime * result; } result = prime * result + fSourcePort; result = prime * result + fTotalLength; return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } UDPPacket other = (UDPPacket) obj; if (fChecksum != other.fChecksum) { return false; } final Packet child = fChildPacket; if (child != null) { if (!child.equals(other.fChildPacket)) { return false; } } else { if (other.fChildPacket != null) { return false; } } if (fDestinationPort != other.fDestinationPort) { return false; } final ByteBuffer payload = fPayload; if (payload != null) { if (!payload.equals(other.fPayload)) { return false; } } else { if (other.fPayload != null) { return false; } } if (fSourcePort != other.fSourcePort) { return false; } if (fTotalLength != other.fTotalLength) { return false; } return true; }
The UDPPacket class is implemented. We now have the define the UDPEndpoint.
For the UDP protocol, an endpoint will be its source or its destination port, depending if it is the source endpoint or destination endpoint. Knowing that, we can create our UDPEndpoint class.
We create in our package a new class named UDPEndpoint that extends ProtocolEndpoint. We also add a field: fPort, which contains the source or destination port. We finally add a constructor public ExampleEndpoint(Packet packet, boolean isSourceEndpoint):
We obtain the following unimplemented class:
package org.eclipse.tracecompass.pcap.core.protocol.udp; import org.eclipse.tracecompass.internal.pcap.core.endpoint.ProtocolEndpoint; import org.eclipse.tracecompass.internal.pcap.core.packet.Packet; public class UDPEndpoint extends ProtocolEndpoint { private final int fPort; public UDPEndpoint(Packet packet, boolean isSourceEndpoint) { super(packet, isSourceEndpoint); // TODO Auto-generated constructor stub } @Override public int hashCode() { // TODO Auto-generated method stub return 0; } @Override public boolean equals(Object obj) { // TODO Auto-generated method stub return false; } @Override public String toString() { // TODO Auto-generated method stub return null; } }
For the constructor, we simply initialize fPort. If isSourceEndpoint is true, then we take packet.getSourcePort(), else we take packet.getDestinationPort().
/** * Constructor of the {@link UDPEndpoint} class. It takes a packet to get * its endpoint. Since every packet has two endpoints (source and * destination), the isSourceEndpoint parameter is used to specify which * endpoint to take. * * @param packet * The packet that contains the endpoints. * @param isSourceEndpoint * Whether to take the source or the destination endpoint of the * packet. */ public UDPEndpoint(UDPPacket packet, boolean isSourceEndpoint) { super(packet, isSourceEndpoint); fPort = isSourceEndpoint ? packet.getSourcePort() : packet.getDestinationPort(); }
Then we implement the methods:
@Override public int hashCode() { final int prime = 31; int result = 1; ProtocolEndpoint endpoint = getParentEndpoint(); if (endpoint == null) { result = 0; } else { result = endpoint.hashCode(); } result = prime * result + fPort; return result; } @Override public boolean equals(@Nullable Object obj) { if (this == obj) { return true; } if (!(obj instanceof UDPEndpoint)) { return false; } UDPEndpoint other = (UDPEndpoint) obj; // Check on layer boolean localEquals = (fPort == other.fPort); if (!localEquals) { return false; } // Check above layers. ProtocolEndpoint endpoint = getParentEndpoint(); if (endpoint != null) { return endpoint.equals(other.getParentEndpoint()); } return true; } @Override public String toString() { ProtocolEndpoint endpoint = getParentEndpoint(); if (endpoint == null) { @SuppressWarnings("null") @NonNull String ret = String.valueOf(fPort); return ret; } return endpoint.toString() + '/' + fPort; }
The last step is to register the new protocol. There are three places where the protocol has to be registered. First, the parser has to know that a new protocol has been added. This is defined in the enum org.eclipse.tracecompass.internal.pcap.core.protocol.PcapProtocol. Simply add the protocol name here, along with a few arguments:
Thus, the following line is added in the PcapProtocol enum:
UDP("User Datagram Protocol", "udp", Layer.LAYER_4, true),
Also, TMF has to know about the new protocol. This is defined in org.eclipse.tracecompass.internal.tmf.pcap.core.protocol.TmfPcapProtocol. We simply add it, with a reference to the corresponding protocol in PcapProtocol. Thus, the following line is added in the TmfPcapProtocol enum:
UDP(PcapProtocol.UDP),
You will also have to update the ProtocolConversion class to register the protocol in the switch statements. Thus, for UDP, we add:
case UDP: return TmfPcapProtocol.UDP;
and
case UDP: return PcapProtocol.UDP;
Finally, all the protocols that could be the parent of the new protocol (in our case, IPv4 and IPv6) have to be notified of the new protocol. This is done by modifying the findChildPacket() method of the packet class of those protocols. For instance, in IPv4Packet, we add a case in the switch statement of findChildPacket, if the Protocol number matches UDP's protocol number at the network layer:
@Override protected @Nullable Packet findChildPacket() throws BadPacketException { ByteBuffer payload = fPayload; if (payload == null) { return null; } switch (fIpDatagramProtocol) { case IPProtocolNumberHelper.PROTOCOL_NUMBER_TCP: return new TCPPacket(getPcapFile(), this, payload); case IPProtocolNumberHelper.PROTOCOL_NUMBER_UDP: return new UDPPacket(getPcapFile(), this, payload); default: return new UnknownPacket(getPcapFile(), this, payload); } }
The new protocol has been added. Running TMF should work just fine, and the new protocol is now recognized.
To add a stream-based View, simply monitor the TmfPacketStreamSelectedSignal in your view. It contains the new stream that you can retrieve with signal.getStream(). You must then make an event request to the current trace to get the events, and use the stream to filter the events of interest. Therefore, you must also monitor TmfTraceOpenedSignal, TmfTraceClosedSignal and TmfTraceSelectedSignal. Examples of stream-based views include a view that represents the packets as a sequence diagram, or that shows the TCP connection state based on the packets SYN/ACK/FIN/RST flags. A (very very very early) draft of such a view can be found at https://git.eclipse.org/r/#/c/31054/.
Performance Tests | Markers |