/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.plc4x.java.profinet.discovery;
import org.apache.plc4x.java.api.exceptions.PlcException;
import org.apache.plc4x.java.api.messages.PlcDiscoveryItem;
import org.apache.plc4x.java.api.messages.PlcDiscoveryItemHandler;
import org.apache.plc4x.java.api.messages.PlcDiscoveryRequest;
import org.apache.plc4x.java.api.messages.PlcDiscoveryResponse;
import org.apache.plc4x.java.api.types.PlcResponseCode;
import org.apache.plc4x.java.profinet.ProfinetDriver;
import org.apache.plc4x.java.profinet.readwrite.*;
import org.apache.plc4x.java.spi.generation.*;
import org.apache.plc4x.java.spi.messages.DefaultPlcDiscoveryItem;
import org.apache.plc4x.java.spi.messages.DefaultPlcDiscoveryResponse;
import org.apache.plc4x.java.spi.messages.PlcDiscoverer;
import org.apache.plc4x.java.transport.rawsocket.RawSocketTransport;
import org.pcap4j.core.*;
import org.pcap4j.packet.Dot1qVlanTagPacket;
import org.pcap4j.packet.EthernetPacket;
import org.pcap4j.packet.IllegalRawDataException;
import org.pcap4j.packet.Packet;
import org.pcap4j.packet.namednumber.EtherType;
import org.pcap4j.util.LinkLayerAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ProfinetPlcDiscoverer implements PlcDiscoverer {
private static final EtherType PN_EtherType = EtherType.getInstance((short) 0x8892);
// The constants for the different block names and their actual meaning.
private static final String DEVICE_TYPE_NAME = "DEVICE_PROPERTIES_OPTION-1";
private static final String DEVICE_NAME_OF_STATION = "DEVICE_PROPERTIES_OPTION-2";
private static final String DEVICE_ID = "DEVICE_PROPERTIES_OPTION-3";
private static final String DEVICE_ROLE = "DEVICE_PROPERTIES_OPTION-4";
private static final String DEVICE_OPTIONS = "DEVICE_PROPERTIES_OPTION-5";
private static final String DEVICE_INSTANCE = "DEVICE_PROPERTIES_OPTION-7";
private static final String IP_OPTION_IP = "IP_OPTION-2";
private final Logger logger = LoggerFactory.getLogger(ProfinetPlcDiscoverer.class);
@Override
public CompletableFuture<PlcDiscoveryResponse> discover(PlcDiscoveryRequest discoveryRequest) {
return discoverWithHandler(discoveryRequest, null);
}
public CompletableFuture<PlcDiscoveryResponse> discoverWithHandler(PlcDiscoveryRequest discoveryRequest, PlcDiscoveryItemHandler handler) {
CompletableFuture<PlcDiscoveryResponse> future = new CompletableFuture<>();
Set<PcapHandle> openHandles = new HashSet<>();
List<PlcDiscoveryItem> values = new ArrayList<>();
try {
for (PcapNetworkInterface dev : Pcaps.findAllDevs()) {
// It turned out on some MAC network devices without any ip addresses
// the compiling of the filter expression was causing errors. As
// currently there was no other way to detect this, this check seems
// to be sufficient.
if(dev.getAddresses().size() == 0) {
continue;
}
if (!dev.isLoopBack()) {
for (LinkLayerAddress linkLayerAddress : dev.getLinkLayerAddresses()) {
org.pcap4j.util.MacAddress macAddress = (org.pcap4j.util.MacAddress) linkLayerAddress;
PcapHandle handle = dev.openLive(65536, PcapNetworkInterface.PromiscuousMode.PROMISCUOUS, 10);
openHandles.add(handle);
ExecutorService pool = Executors.newSingleThreadExecutor();
// Only react on PROFINET DCP packets targeted at our current MAC address.
handle.setFilter(
"((ether proto 0x8100) or (ether proto 0x8892)) and (ether dst " + Pcaps.toBpfString(macAddress) + ")",
BpfProgram.BpfCompileMode.OPTIMIZE);
PacketListener listener =
packet -> {
// EthernetPacket is the highest level of abstraction we can be expecting.
// Everything inside this we will have to decode ourselves.
if (packet instanceof EthernetPacket) {
EthernetPacket ethernetPacket = (EthernetPacket) packet;
boolean isPnPacket = false;
// I have observed some times the ethernet packets being wrapped inside a VLAN
// Packet, in this case we simply unpack the content.
if (ethernetPacket.getPayload() instanceof Dot1qVlanTagPacket) {
Dot1qVlanTagPacket vlanPacket = (Dot1qVlanTagPacket) ethernetPacket.getPayload();
if (PN_EtherType.equals(vlanPacket.getHeader().getType())) {
isPnPacket = true;
}
} else if (PN_EtherType.equals(ethernetPacket.getHeader().getType())) {
isPnPacket = true;
}
// It's a PROFINET packet.
if (isPnPacket) {
ReadBuffer reader = new ReadBufferByteBased(ethernetPacket.getRawData());
try {
Ethernet_Frame ethernetFrame = Ethernet_Frame.staticParse(reader);
PnDcp_Pdu pdu;
// Access the pdu data (either directly or by
// unpacking the content of the VLAN packet.
if (ethernetFrame.getPayload() instanceof Ethernet_FramePayload_VirtualLan) {
Ethernet_FramePayload_VirtualLan vlefpl = (Ethernet_FramePayload_VirtualLan) ethernetFrame.getPayload();
pdu = ((Ethernet_FramePayload_PnDcp) vlefpl.getPayload()).getPdu();
} else {
pdu = ((Ethernet_FramePayload_PnDcp) ethernetFrame.getPayload()).getPdu();
}
// Inspect the PDU itself
// (in this case we only process identify response packets)
if (pdu instanceof PnDcp_Pdu_IdentifyRes) {
PnDcp_Pdu_IdentifyRes identifyResPDU = (PnDcp_Pdu_IdentifyRes) pdu;
Map<String, PnDcp_Block> blocks = new HashMap<>();
for (PnDcp_Block block : identifyResPDU.getBlocks()) {
String blockName = block.getOption().name() + "-" + block.getSuboption().toString();
blocks.put(blockName, block);
}
// The mac address of the device we found
org.pcap4j.util.MacAddress srcAddr = ethernetPacket.getHeader().getSrcAddr();
// The mac address of the local network device
org.pcap4j.util.MacAddress dstAddr = ethernetPacket.getHeader().getDstAddr();
String deviceTypeName = "unknown";
if (blocks.containsKey(DEVICE_TYPE_NAME)) {
PnDcp_Block_DevicePropertiesDeviceVendor block = (PnDcp_Block_DevicePropertiesDeviceVendor) blocks.get(DEVICE_TYPE_NAME);
deviceTypeName = new String(block.getDeviceVendorValue()).replace(" ", "%20");
}
String deviceName = "unknown";
if (blocks.containsKey(DEVICE_NAME_OF_STATION)) {
PnDcp_Block_DevicePropertiesNameOfStation block = (PnDcp_Block_DevicePropertiesNameOfStation) blocks.get(DEVICE_NAME_OF_STATION);
deviceName = new String(block.getNameOfStation()).replace(" ", "%20");
}
String role = "unknown";
if (blocks.containsKey(DEVICE_ROLE)) {
role = "";
PnDcp_Block_DevicePropertiesDeviceRole block = (PnDcp_Block_DevicePropertiesDeviceRole) blocks.get(DEVICE_ROLE);
if (block.getPnioSupervisor()) {
role += ",SUPERVISOR";
}
if (block.getPnioMultidevive()) {
role += ",MULTIDEVICE";
}
if (block.getPnioController()) {
role += ",CONTROLLER";
}
if (block.getPnioDevice()) {
role += ",DEVICE";
}
// Cut off the first comma
if (role.length() > 0) {
role = role.substring(1);
} else {
role = "unknown";
}
}
String remoteIpAddress = "unknown";
String remoteSubnetMask = "unknown";
if (blocks.containsKey(IP_OPTION_IP)) {
PnDcp_Block_IpParameter block = (PnDcp_Block_IpParameter) blocks.get(IP_OPTION_IP);
try {
InetAddress addr = InetAddress.getByAddress(block.getIpAddress());
remoteIpAddress = addr.getHostAddress();
InetAddress netMask = InetAddress.getByAddress(block.getSubnetMask());
remoteSubnetMask = netMask.getHostAddress();
} catch (UnknownHostException e) {
remoteIpAddress = "invalid";
}
}
// Get the Vendor Id and the Device Id
String vendorId = "unknown";
String deviceId = "unknown";
if (blocks.containsKey(DEVICE_ID)) {
PnDcp_Block_DevicePropertiesDeviceId block = (PnDcp_Block_DevicePropertiesDeviceId) blocks.get(DEVICE_ID);
vendorId = String.format("%04X", block.getVendorId());
deviceId = String.format("%04X", block.getDeviceId());
}
Map<String, String> options = new HashMap<>();
options.put("remoteIpAddress", remoteIpAddress);
options.put("remoteSubnetMask", remoteSubnetMask);
options.put("remoteMacAddress", srcAddr.toString());
options.put("localMacAddress", dstAddr.toString());
options.put("deviceTypeName", deviceTypeName);
options.put("deviceName", deviceName);
options.put("vendorId", vendorId);
options.put("deviceId", deviceId);
options.put("role", role);
String name = deviceTypeName + " - " + deviceName;
PlcDiscoveryItem value = new DefaultPlcDiscoveryItem(
ProfinetDriver.DRIVER_CODE, RawSocketTransport.TRANSPORT_CODE,
remoteIpAddress, options, name, Collections.emptyMap());
values.add(value);
// If we have a discovery handler, pass it to the handler callback
if (handler != null) {
handler.handle(value);
}
logger.debug("Found new device: '{}' with connection-url '{}'",
value.getName(), value.getConnectionUrl());
}
} catch (ParseException e) {
logger.error("Got error decoding packet", e);
}
}
}
};
Task t = new Task(handle, listener);
pool.execute(t);
// Construct and send the search request.
Ethernet_Frame identificationRequest = new Ethernet_Frame(
// Pre-Defined PROFINET discovery MAC address
new MacAddress(new byte[]{0x01, 0x0E, (byte) 0xCF, 0x00, 0x00, 0x00}),
toPlc4xMacAddress(macAddress),
new Ethernet_FramePayload_VirtualLan(VirtualLanPriority.BEST_EFFORT, false, 0,
new Ethernet_FramePayload_PnDcp(
new PnDcp_Pdu_IdentifyReq(PnDcp_FrameId.DCP_Identify_ReqPDU.getValue(),
1,
256,
Collections.singletonList(
new PnDcp_Block_ALLSelector()
)))));
WriteBufferByteBased buffer = new WriteBufferByteBased(34);
identificationRequest.serialize(buffer);
Packet packet = EthernetPacket.newPacket(buffer.getData(), 0, 34);
handle.sendPacket(packet);
}
}
}
} catch (IllegalRawDataException | NotOpenException | PcapNativeException | SerializationException e) {
logger.error("Got an exception while processing raw socket data", e);
future.completeExceptionally(new PlcException("Got an internal error while performing discovery"));
for (PcapHandle openHandle : openHandles) {
openHandle.close();
}
return future;
}
// Create a timer that completes the future after a given time with all the responses it found till then.
Timer timer = new Timer("Discovery Timeout");
timer.schedule(new TimerTask() {
public void run() {
PlcDiscoveryResponse response =
new DefaultPlcDiscoveryResponse(discoveryRequest, PlcResponseCode.OK, values);
future.complete(response);
for (PcapHandle openHandle : openHandles) {
openHandle.close();
}
}
}, 5000L);
return future;
}
private static MacAddress toPlc4xMacAddress(org.pcap4j.util.MacAddress pcap4jMacAddress) {
byte[] address = pcap4jMacAddress.getAddress();
return new MacAddress(new byte[]{address[0], address[1], address[2], address[3], address[4], address[5]});
}
private static class Task implements Runnable {
private final Logger logger = LoggerFactory.getLogger(Task.class);
private final PcapHandle handle;
private final PacketListener listener;
public Task(PcapHandle handle, PacketListener listener) {
this.handle = handle;
this.listener = listener;
}
@Override
public void run() {
try {
handle.loop(10, listener);
} catch (InterruptedException e) {
logger.error("Got error handling raw socket", e);
Thread.currentThread().interrupt();
} catch (PcapNativeException | NotOpenException e) {
logger.error("Got error handling raw socket", e);
}
}
}
public static void main(String[] args) throws Exception {
ProfinetPlcDiscoverer discoverer = new ProfinetPlcDiscoverer();
discoverer.discover(null);
Thread.sleep(10000);
}
}
↑ V6046 Incorrect format. A different number of format items is expected. Arguments not used: 1.
↑ V6046 Incorrect format. A different number of format items is expected. Arguments not used: 1.