/*
* 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.protocol;
import io.netty.channel.Channel;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.plc4x.java.api.exceptions.PlcException;
import org.apache.plc4x.java.api.messages.*;
import org.apache.plc4x.java.profinet.context.ProfinetDriverContext;
import org.apache.plc4x.java.profinet.readwrite.*;
import org.apache.plc4x.java.spi.ConversationContext;
import org.apache.plc4x.java.spi.Plc4xProtocolBase;
import org.apache.plc4x.java.spi.generation.*;
import org.apache.plc4x.java.utils.rawsockets.netty.RawSocketChannel;
import org.pcap4j.core.PcapAddress;
import org.pcap4j.core.PcapNativeException;
import org.pcap4j.core.PcapNetworkInterface;
import org.pcap4j.core.Pcaps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.*;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
public class ProfinetProtocolLogic extends Plc4xProtocolBase<Ethernet_Frame> {
public static final Duration REQUEST_TIMEOUT = Duration.ofMillis(10000);
private static AtomicInteger sessionKeyGenerator = new AtomicInteger(1);
private final Logger logger = LoggerFactory.getLogger(ProfinetProtocolLogic.class);
private ProfinetDriverContext profinetDriverContext;
@Override
public void setContext(ConversationContext<Ethernet_Frame> context) {
super.setContext(context);
this.profinetDriverContext = (ProfinetDriverContext) driverContext;
}
@Override
public void onConnect(ConversationContext<Ethernet_Frame> context) {
final Channel channel = context.getChannel();
if (!(channel instanceof RawSocketChannel)) {
logger.warn("Expected a 'raw' transport, closing channel...");
context.getChannel().close();
return;
}
RawSocketChannel rawSocketChannel = (RawSocketChannel) channel;
// Create an udp socket
DatagramSocket udpSocket;
try {
udpSocket = new DatagramSocket();
} catch (SocketException e) {
logger.warn("Unable to create udp socket " + e.getMessage());
context.getChannel().close();
return;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Initialize some important datastructures, that will be used a lot.
// Generate a new Activity Id, which will be used throughout the connection.
profinetDriverContext.setDceRpcActivityUuid(generateActivityUuid());
// TODO: Possibly we can remove the ARP lookup and simply use the mac address in the connection-response.
// Local connectivity attributes
profinetDriverContext.setLocalMacAddress(new MacAddress(rawSocketChannel.getLocalMacAddress().getAddress()));
final InetSocketAddress localAddress = (InetSocketAddress) rawSocketChannel.getLocalAddress();
Inet4Address localIpAddress = (Inet4Address) localAddress.getAddress();
profinetDriverContext.setLocalIpAddress(new IpAddress(localIpAddress.getAddress()));
// Use the port of the udp socket
profinetDriverContext.setLocalUdpPort(udpSocket.getPort());
// Remote connectivity attributes
byte[] macAddress = null;
try {
macAddress = Hex.decodeHex("000000000000");
} catch (DecoderException e) {
// Ignore this.
}
profinetDriverContext.setRemoteMacAddress(new MacAddress(macAddress));
final InetSocketAddress remoteAddress = (InetSocketAddress) rawSocketChannel.getRemoteAddress();
Inet4Address remoteIpAddress = (Inet4Address) remoteAddress.getAddress();
profinetDriverContext.setRemoteIpAddress(new IpAddress(remoteIpAddress.getAddress()));
profinetDriverContext.setRemoteUdpPort(remoteAddress.getPort());
// Generate a new session key.
profinetDriverContext.setSessionKey(sessionKeyGenerator.getAndIncrement());
// Reset the session key as soon as it reaches the max for a 16 bit uint
if (sessionKeyGenerator.get() == 0xFFFF) {
sessionKeyGenerator.set(1);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Create the connection request.
try {
// Create the packet
final DceRpc_Packet profinetConnectionRequest = createProfinetConnectionRequest();
// Serialize it to a byte-payload
WriteBufferByteBased writeBuffer = new WriteBufferByteBased(profinetConnectionRequest.getLengthInBytes());
profinetConnectionRequest.serialize(writeBuffer);
// Create a udp packet.
DatagramPacket connectRequestPacket = new DatagramPacket(writeBuffer.getData(), writeBuffer.getData().length);
connectRequestPacket.setAddress(remoteAddress.getAddress());
connectRequestPacket.setPort(remoteAddress.getPort());
// Send it.
udpSocket.send(connectRequestPacket);
// Receive the response.
byte[] resultBuffer = new byte[profinetConnectionRequest.getLengthInBytes()];
DatagramPacket connectResponsePacket = new DatagramPacket(resultBuffer, resultBuffer.length);
udpSocket.receive(connectResponsePacket);
ReadBufferByteBased readBuffer = new ReadBufferByteBased(resultBuffer);
final DceRpc_Packet dceRpc_packet = DceRpc_Packet.staticParse(readBuffer);
if ((dceRpc_packet.getOperation() == DceRpc_Operation.CONNECT) && (dceRpc_packet.getPacketType() == DceRpc_PacketType.RESPONSE)) {
if (dceRpc_packet.getPayload().getPacketType() == DceRpc_PacketType.RESPONSE) {
// Get the remote MAC address and store it in the context.
final PnIoCm_Packet_Res connectResponse = (PnIoCm_Packet_Res) dceRpc_packet.getPayload();
if ((connectResponse.getBlocks().size() > 0) && (connectResponse.getBlocks().get(0) instanceof PnIoCm_Block_ArRes)) {
final PnIoCm_Block_ArRes pnIoCm_block_arRes = (PnIoCm_Block_ArRes) connectResponse.getBlocks().get(0);
profinetDriverContext.setRemoteMacAddress(pnIoCm_block_arRes.getCmResponderMacAddr());
// Update the raw-socket transports filter expression.
((RawSocketChannel) channel).setRemoteMacAddress(org.pcap4j.util.MacAddress.getByAddress(profinetDriverContext.getRemoteMacAddress().getAddress()));
} else {
throw new PlcException("Unexpected type of first block.");
}
} else {
throw new PlcException("Unexpected response");
}
} else if (dceRpc_packet.getPacketType() == DceRpc_PacketType.REJECT) {
throw new PlcException("Device rejected connection request");
} else {
throw new PlcException("Unexpected response");
}
} catch (SerializationException | IOException | PlcException | ParseException e) {
logger.error("Error", e);
}
//System.out.println(rawSocketChannel);
}
@Override
public void close(ConversationContext<Ethernet_Frame> context) {
// Nothing to do here ...
}
@Override
public CompletableFuture<PlcReadResponse> read(PlcReadRequest readRequest) {
CompletableFuture<PlcReadResponse> future = new CompletableFuture<>();
future.completeExceptionally(new NotImplementedException());
return future;
}
@Override
public CompletableFuture<PlcWriteResponse> write(PlcWriteRequest writeRequest) {
CompletableFuture<PlcWriteResponse> future = new CompletableFuture<>();
future.completeExceptionally(new NotImplementedException());
return future;
}
@Override
protected void decode(ConversationContext<Ethernet_Frame> context, Ethernet_Frame msg) throws Exception {
super.decode(context, msg);
}
private Optional<PcapNetworkInterface> getNetworkInterfaceForConnection(InetAddress address) {
try {
for (PcapNetworkInterface dev : Pcaps.findAllDevs()) {
// We're only interested in real running network interfaces, skip the rest.
if (dev.isLoopBack() || !dev.isRunning() || dev.isUp()) {
continue;
}
for (PcapAddress curAddress : dev.getAddresses()) {
}
}
} catch (PcapNativeException e) {
logger.warn(String.format("Error finding network device for connection to %s", address.toString()), e);
}
return Optional.empty();
}
private DceRpc_Packet createProfinetConnectionRequest() throws PlcException {
try {
return new DceRpc_Packet(
DceRpc_PacketType.REQUEST, true, false, false,
IntegerEncoding.BIG_ENDIAN, CharacterEncoding.ASCII, FloatingPointEncoding.IEEE,
new DceRpc_ObjectUuid((byte) 0x00, 0x0001, 0x0904, 0x002A),
new DceRpc_InterfaceUuid_DeviceInterface(),
profinetDriverContext.getDceRpcActivityUuid(),
0, 0, DceRpc_Operation.CONNECT,
new PnIoCm_Packet_Req(404, 404, 404, 0, 404,
Arrays.asList(
new PnIoCm_Block_ArReq((short) 1, (short) 0, PnIoCm_ArType.IO_CONTROLLER,
new Uuid(Hex.decodeHex("654519352df3b6428f874371217c2b51")),
profinetDriverContext.getSessionKey(),
profinetDriverContext.getLocalMacAddress(),
new Uuid(Hex.decodeHex("dea000006c9711d1827100640008002a")),
false, false, false,
false, PnIoCm_CompanionArType.SINGLE_AR, false,
true, false, PnIoCm_State.ACTIVE,
600,
// This actually needs to be set to this value and not the real port number.
0x8892,
// It seems that it must be set to this value, or it won't work.
"profinetxadriver4933"),
new PnIoCm_Block_IoCrReq((short) 1, (short) 0, PnIoCm_IoCrType.INPUT_CR,
0x0001,
0x8892,
false, false,
false, false, PnIoCm_RtClass.RT_CLASS_2, 40,
0xBBF0, 128, 8, 1, 0, 0xffffffff,
3, 3, 0xC000,
new org.apache.plc4x.java.profinet.readwrite.MacAddress(Hex.decodeHex("000000000000")),
Collections.singletonList(
new PnIoCm_IoCrBlockReqApi(
Arrays.asList(
new PnIoCm_IoDataObject(0, 0x0001, 0),
new PnIoCm_IoDataObject(0, 0x8000, 1),
new PnIoCm_IoDataObject(0, 0x8001, 2),
new PnIoCm_IoDataObject(0, 0x8002, 3),
new PnIoCm_IoDataObject(1, 0x0001, 4)
),
Collections.singletonList(
new PnIoCm_IoCs(0x0001, 0x0001, 0x0019)
))
)),
new PnIoCm_Block_IoCrReq((short) 1, (short) 0, PnIoCm_IoCrType.OUTPUT_CR,
0x0002, 0x8892, false, false,
false, false, PnIoCm_RtClass.RT_CLASS_2, 40,
0x8000, 128, 8, 1, 0, 0xffffffff,
3, 3, 0xC000,
new MacAddress(Hex.decodeHex("000000000000")),
Collections.singletonList(
new PnIoCm_IoCrBlockReqApi(
Collections.singletonList(
new PnIoCm_IoDataObject(0x0001, 0x0001, 0x0005)
),
Arrays.asList(
new PnIoCm_IoCs(0, 0x0001, 0),
new PnIoCm_IoCs(0, 0x8000, 1),
new PnIoCm_IoCs(0, 0x8001, 2),
new PnIoCm_IoCs(0, 0x8002, 3),
new PnIoCm_IoCs(1, 0x0001, 4)
)
)
)
),
new PnIoCm_Block_ExpectedSubmoduleReq((short) 1, (short) 0,
Collections.singletonList(
new PnIoCm_ExpectedSubmoduleBlockReqApi(0,
0x00000010, 0x00000000,
Arrays.asList(
new PnIoCm_Submodule_NoInputNoOutputData(0x0001,
0x00000001, false, false,
false, false),
new PnIoCm_Submodule_NoInputNoOutputData(0x8000,
0x00000002, false, false,
false, false),
new PnIoCm_Submodule_NoInputNoOutputData(0x8001,
0x00000003, false, false,
false, false),
new PnIoCm_Submodule_NoInputNoOutputData(0x8002,
0x00000003, false, false,
false, false)
)
)
)
),
new PnIoCm_Block_ExpectedSubmoduleReq((short) 1, (short) 0,
Collections.singletonList(
new PnIoCm_ExpectedSubmoduleBlockReqApi(1,
0x00000022, 0x00000000, Collections.singletonList(
new PnIoCm_Submodule_InputAndOutputData(0x0001, 0x00000010,
false, false, false,
false, 20, (short) 1, (short) 1,
6, (short) 1, (short) 1))
)
)
),
new PnIoCm_Block_AlarmCrReq((short) 1, (short) 0,
PnIoCm_AlarmCrType.ALARM_CR, 0x8892, false, false, 1, 3,
0x0000, 200, 0xC000, 0xA000)
))
);
/*// Build the UDP/IP/EthernetFrame to transport the package.
return new Ethernet_Frame(profinetDriverContext.getRemoteMacAddress(), profinetDriverContext.getLocalMacAddress(),
new Ethernet_FramePayload_IPv4(ThreadLocalRandom.current().nextInt(0, Integer.MAX_VALUE), (short) 64,
profinetDriverContext.getLocalIpAddress(), profinetDriverContext.getRemoteIpAddress(),
new Udp_Packet(profinetDriverContext.getLocalUdpPort(), profinetDriverContext.getRemoteUdpPort(),
dceRpcConnectionRequest)));*/
} catch (DecoderException e) {
throw new PlcException("Error creating connection request", e);
}
}
protected static DceRpc_ActivityUuid generateActivityUuid() {
UUID number = UUID.randomUUID();
try {
WriteBufferByteBased wb = new WriteBufferByteBased(128);
wb.writeLong(64, number.getMostSignificantBits());
wb.writeLong(64, number.getLeastSignificantBits());
ReadBuffer rb = new ReadBufferByteBased(wb.getData());
return new DceRpc_ActivityUuid(rb.readLong(32), rb.readInt(16), rb.readInt(16), rb.readByteArray(8));
} catch (SerializationException | ParseException e) {
// Ignore ... this should actually never happen.
}
return null;
}
}
↑ V6046 Incorrect format. A different number of format items is expected. Arguments not used: 1.