diff --git a/CMakeLists.txt b/CMakeLists.txt index 4dda3c8f..701c3299 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,11 @@ project(EIPScanner HOMEPAGE_URL "https://github.com/nimbuscontrols/EIPScanner" ) -set(CMAKE_CXX_STANDARD 20) +# C++17: the Luckfox/RV1106 SDK cross toolchain is GCC 8.3 (no C++20). The +# codebase uses no C++20-only features, so this is a clean downgrade. +# (A bare set() here would otherwise override -DCMAKE_CXX_STANDARD from buildroot.) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) option(ENABLE_VENDOR_SRC "Enable vendor source" ON) option(TEST_ENABLED "Enable unit test" OFF) option(EXAMPLE_ENABLED "Build examples" OFF) diff --git a/README.md b/README.md index 37711a14..3e8bbc70 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ Vendor specific objects: ## Requirements * CMake 3.5 and higher -* C++20 compiler (tested with GCC and MinGW) +* C++17 compiler (this fork: downgraded from upstream C++20 for the + Luckfox/RV1106 SDK toolchain, GCC 8.3 / uClibc-ng; no C++20-only features used) * Linux, MacOS, and Windows ## Installing diff --git a/src/ConnectionManager.cpp b/src/ConnectionManager.cpp index a4c76a20..868e9f32 100644 --- a/src/ConnectionManager.cpp +++ b/src/ConnectionManager.cpp @@ -6,6 +6,13 @@ #include #include +#if defined(__unix__) || defined(__APPLE__) +#include +#include +#elif defined(_WIN32) || defined(WIN32) || defined(_WIN64) +#include +#endif + #include "ConnectionManager.h" #include "eip/CommonPacket.h" #include "cip/connectionManager/ForwardOpenRequest.h" @@ -28,6 +35,12 @@ namespace eipScanner { using sockets::UDPBoundSocket; using sockets::BaseSocket; + // IPv4 multicast range 224.0.0.0 .. 239.255.255.255 (class D). + static bool isMulticastAddr(const struct in_addr& a) { + uint32_t h = ntohl(a.s_addr); + return h >= 0xE0000000u && h <= 0xEFFFFFFFu; + } + enum class ConnectionManagerServiceCodes : cip::CipUsint { FORWARD_OPEN = 0x54, LARGE_FORWARD_OPEN = 0x5B, @@ -75,8 +88,18 @@ namespace eipScanner { if ((connectionParameters.transportTypeTrigger & NetworkConnectionParams::CLASS1) > 0 || (connectionParameters.transportTypeTrigger & NetworkConnectionParams::CLASS3) > 0) { - connectionParameters.o2tNetworkConnectionParams += 2; - connectionParameters.t2oNetworkConnectionParams += 2; + // The +2 reserves the 2-byte sequence count that Class 1/3 I/O data + // carries. A NULL connection (e.g. the Input-Only / Listen-Only O2T + // heartbeat — assembly instance 198 on the Joral encoder, "0 bytes") + // transfers no data and no sequence count: adding 2 would make a + // 0-byte heartbeat advertise connection size 2, which conformant + // adapters reject with Forward_Open extended status 0x0123 (invalid + // O2T network connection type). Only reserve the count on a real + // (non-null) data leg. + if (o2tNCP.getConnectionType() != NetworkConnectionParametersBuilder::NULL_TYPE) + connectionParameters.o2tNetworkConnectionParams += 2; + if (t2oNCP.getConnectionType() != NetworkConnectionParametersBuilder::NULL_TYPE) + connectionParameters.t2oNetworkConnectionParams += 2; } if (connectionParameters.o2tRealTimeFormat) { @@ -151,7 +174,27 @@ namespace eipScanner { Logger(LogLevel::INFO) << "Open UDP socket to send data to " << ioConnection->_socket->getRemoteEndPoint().toString(); - findOrCreateSocket(sockets::EndPoint(si->getRemoteEndPoint().getHost(), EIP_DEFAULT_IMPLICIT_PORT)); + // Set up the T2O receive socket. If the target advertised a multicast + // T2O_SOCKADDR_INFO, join that group; otherwise receive unicast on the + // implicit port (legacy point-to-point behaviour). + auto t2oSockAddrInfo = std::find_if(additionalItems.begin(), additionalItems.end(), + [](auto item) { return item.getTypeId() == eip::CommonPacketItemIds::T2O_SOCKADDR_INFO; }); + + bool t2oMulticast = false; + if (t2oSockAddrInfo != additionalItems.end()) { + Buffer t2oSockAddrBuffer(t2oSockAddrInfo->getData()); + sockets::EndPoint t2oEndPoint("", 0); + t2oSockAddrBuffer >> t2oEndPoint; + + if (isMulticastAddr(t2oEndPoint.getAddr().sin_addr)) { + findOrCreateMulticastSocket(t2oEndPoint); + t2oMulticast = true; + } + } + + if (!t2oMulticast) { + findOrCreateSocket(sockets::EndPoint(si->getRemoteEndPoint().getHost(), EIP_DEFAULT_IMPLICIT_PORT)); + } auto result = _connectionMap .insert(std::make_pair(response.getT2ONetworkConnectionId(), ioConnection)); @@ -226,41 +269,64 @@ namespace eipScanner { } } + void ConnectionManager::attachIoReceiveHandler(const UDPBoundSocket::SPtr& socket) { + socket->setBeginReceiveHandler([this](BaseSocket& sock) { + auto recvData = sock.Receive(8192); + CommonPacket commonPacket; + commonPacket.expand(recvData); + + const auto& items = commonPacket.getItems(); + if (items.size() < 2) { + Logger(LogLevel::WARNING) << "Received malformed I/O CommonPacket: expected >=2 items, got " << items.size(); + return; + } + + // TODO: Check TypeIDs and sequence of the packages + Buffer buffer(items[0].getData()); + cip::CipUdint connectionId; + buffer >> connectionId; + Logger(LogLevel::DEBUG) << "Received data from connection T2O_ID=" << connectionId; + + auto io = _connectionMap.find(connectionId); + if (io != _connectionMap.end()) { + io->second->notifyReceiveData(items[1].getData()); + } else { + Logger(LogLevel::ERROR) << "Received data from unknown connection T2O_ID=" << connectionId; + } + }); + } + UDPBoundSocket::SPtr ConnectionManager::findOrCreateSocket(const sockets::EndPoint& endPoint) { auto socket = _socketMap.find(endPoint); if (socket == _socketMap.end()) { auto newSocket = std::make_shared(endPoint); _socketMap[endPoint] = newSocket; - newSocket->setBeginReceiveHandler([](sockets::BaseSocket& sock) { - (void) sock; - Logger(LogLevel::DEBUG) << "Received something"; - }); - - newSocket->setBeginReceiveHandler([this](BaseSocket& sock) { - auto recvData = sock.Receive(8192); - CommonPacket commonPacket; - commonPacket.expand(recvData); - - // TODO: Check TypeIDs and sequence of the packages - Buffer buffer(commonPacket.getItems().at(0).getData()); - cip::CipUdint connectionId; - buffer >> connectionId; - Logger(LogLevel::DEBUG) << "Received data from connection T2O_ID=" << connectionId; - - auto io = _connectionMap.find(connectionId); - if (io != _connectionMap.end()) { - io->second->notifyReceiveData(commonPacket.getItems().at(1).getData()); - } else { - Logger(LogLevel::ERROR) << "Received data from unknown connection T2O_ID=" << connectionId; - } - }); - + attachIoReceiveHandler(newSocket); return newSocket; } return socket->second; } + // Receive T2O over a multicast group: bind to the group address/port + // (to avoid capturing unicast datagrams on the same port) and join the group + // the target advertised in its T2O_SOCKADDR_INFO. + UDPBoundSocket::SPtr ConnectionManager::findOrCreateMulticastSocket(const sockets::EndPoint& groupEndPoint) { + auto socket = _socketMap.find(groupEndPoint); + if (socket != _socketMap.end()) { + return socket->second; + } + + auto newSocket = std::make_shared(groupEndPoint, /*bindToGroup=*/true); + newSocket->joinMulticastGroup(groupEndPoint.getAddr().sin_addr); + _socketMap[groupEndPoint] = newSocket; + attachIoReceiveHandler(newSocket); + + Logger(LogLevel::INFO) << "Joined multicast group " << groupEndPoint.toString() + << " for T2O reception"; + return newSocket; + } + bool ConnectionManager::hasOpenConnections() const { return !_connectionMap.empty(); } diff --git a/src/ConnectionManager.h b/src/ConnectionManager.h index a317067a..4ff4d3e1 100644 --- a/src/ConnectionManager.h +++ b/src/ConnectionManager.h @@ -79,6 +79,8 @@ namespace eipScanner { std::map> _socketMap; sockets::UDPBoundSocket::SPtr findOrCreateSocket(const sockets::EndPoint& endPoint); + sockets::UDPBoundSocket::SPtr findOrCreateMulticastSocket(const sockets::EndPoint& groupEndPoint); + void attachIoReceiveHandler(const sockets::UDPBoundSocket::SPtr& socket); cip::CipUint _incarnationId; }; } diff --git a/src/sockets/BaseSocket.cpp b/src/sockets/BaseSocket.cpp index efb71db2..c3a7ecf5 100644 --- a/src/sockets/BaseSocket.cpp +++ b/src/sockets/BaseSocket.cpp @@ -13,6 +13,7 @@ #include #include +#include #include "BaseSocket.h" #include "Platform.h" @@ -118,6 +119,15 @@ namespace sockets { } void BaseSocket::select(std::vector sockets, std::chrono::milliseconds timeout) { + // With no sockets to wait on (e.g. no connection is open yet, or the last + // Forward_Open was rejected), there is nothing to select(). Sleep out the + // timeout and return — dereferencing *max_element() of an empty range is + // undefined behaviour and segfaulted the whole process here. + if (sockets.empty()) { + std::this_thread::sleep_for(timeout); + return; + } + BaseSocket::SPtr socketWithMaxFd = *std::max_element(sockets.begin(), sockets.end(), [](auto sock1, auto sock2) { return sock1->getSocketFd() < sock2->getSocketFd(); }); diff --git a/src/sockets/UDPBoundSocket.cpp b/src/sockets/UDPBoundSocket.cpp index 8b03fc91..65581bc2 100644 --- a/src/sockets/UDPBoundSocket.cpp +++ b/src/sockets/UDPBoundSocket.cpp @@ -2,9 +2,14 @@ // Created by Aleksey Timin on 11/21/19. // #include +#include -//#include -//#include +#if defined(__unix__) || defined(__APPLE__) +#include +#include +#elif defined(_WIN32) || defined(WIN32) || defined(_WIN64) +#include +#endif #include "UDPBoundSocket.h" #include "Platform.h" @@ -17,7 +22,7 @@ namespace sockets { : UDPBoundSocket(EndPoint(host, port)) { } - UDPBoundSocket::UDPBoundSocket(EndPoint endPoint) + UDPBoundSocket::UDPBoundSocket(EndPoint endPoint, bool bindToGroup) : UDPSocket(std::move(endPoint)) { int on = 1; if (setsockopt(_sockedFd, SOL_SOCKET, SO_REUSEADDR, (char *) &on, sizeof(on)) < 0) { @@ -25,12 +30,39 @@ namespace sockets { } auto addr = _remoteEndPoint.getAddr(); - addr.sin_addr.s_addr = INADDR_ANY; + // Unicast: bind the port on any local address. Multicast: bind to the + // group address so the socket only receives that group (and does not + // steal unicast datagrams on the same port). + if (!bindToGroup) { + addr.sin_addr.s_addr = INADDR_ANY; + } if (bind(_sockedFd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { throw std::system_error(BaseSocket::getLastError(), BaseSocket::getErrorCategory()); } } - sockets::UDPBoundSocket::~UDPBoundSocket() = default; + void UDPBoundSocket::joinMulticastGroup(const struct in_addr& group) { + struct ip_mreq mreq; + std::memset(&mreq, 0, sizeof(mreq)); + mreq.imr_multiaddr = group; + mreq.imr_interface.s_addr = htonl(INADDR_ANY); + if (setsockopt(_sockedFd, IPPROTO_IP, IP_ADD_MEMBERSHIP, + (char *) &mreq, sizeof(mreq)) < 0) { + throw std::system_error(BaseSocket::getLastError(), BaseSocket::getErrorCategory()); + } + _multicastGroup = group; + _joinedMulticast = true; + } + + sockets::UDPBoundSocket::~UDPBoundSocket() { + if (_joinedMulticast) { + struct ip_mreq mreq; + std::memset(&mreq, 0, sizeof(mreq)); + mreq.imr_multiaddr = _multicastGroup; + mreq.imr_interface.s_addr = htonl(INADDR_ANY); + setsockopt(_sockedFd, IPPROTO_IP, IP_DROP_MEMBERSHIP, + (char *) &mreq, sizeof(mreq)); + } + } } } diff --git a/src/sockets/UDPBoundSocket.h b/src/sockets/UDPBoundSocket.h index 3d9c3f9f..ebd24eb4 100644 --- a/src/sockets/UDPBoundSocket.h +++ b/src/sockets/UDPBoundSocket.h @@ -19,9 +19,20 @@ namespace sockets { using WPtr = std::weak_ptr; using SPtr = std::shared_ptr; - explicit UDPBoundSocket(EndPoint endPoint); + // bindToGroup=true binds to endPoint's address (a multicast group) so + // the socket only receives that group's datagrams; false binds + // INADDR_ANY (the legacy unicast-receive behaviour). + explicit UDPBoundSocket(EndPoint endPoint, bool bindToGroup = false); UDPBoundSocket(std::string host, int port); virtual ~UDPBoundSocket(); + + // Join an IPv4 multicast group on this bound socket so a target's + // T2O multicast producer is received. Dropped on destruction. + void joinMulticastGroup(const struct in_addr& group); + + private: + struct in_addr _multicastGroup{}; + bool _joinedMulticast = false; }; } }