PROFINET IO Controller Stack 1.0.0
Modern C++ implementation of a PROFINET IO Controller stack
Loading...
Searching...
No Matches
ethernetPacketBackend.cpp
Go to the documentation of this file.
1
3
5
6#include <algorithm>
7#include <limits>
8#include <string>
9#include <utility>
10
11#include "profinet/exceptions.h"
12#include "profinet/util.h"
13
14namespace profinet
15{
16namespace
17{
18constexpr std::size_t kEthernetHeaderSize = 14U;
19constexpr int kCaptureTimeoutMilliseconds = 1;
20
21[[noreturn]] void ThrowPcapError(const std::string& operation, pcap_t* capture, int errorCode)
22{
23 const char* detail = capture != nullptr ? pcap_geterr(capture) : nullptr;
24
25 std::string message = operation;
26
27 if (detail != nullptr && *detail != '\0')
28 {
29 message += ": ";
30 message += detail;
31 }
32 else
33 {
34 message += " (libpcap error " + std::to_string(errorCode) + ")";
35 }
36
37 if (errorCode == PCAP_ERROR_PERM_DENIED || errorCode == PCAP_ERROR_PROMISC_PERM_DENIED)
38 {
39 throw PermissionDeniedError(message);
40 }
41
42 throw SocketError(message);
43}
44
49std::string ResolveCaptureDevice(const std::string& requested)
50{
51 char errorBuffer[PCAP_ERRBUF_SIZE]{};
52
53 pcap_if_t* devices = nullptr;
54
55 const int result = pcap_findalldevs(&devices, errorBuffer);
56
57 if (result != 0)
58 {
59 throw SocketError("Failed to enumerate packet-capture devices: " + std::string(errorBuffer));
60 }
61
62 std::string resolved = requested;
63
64 for (const pcap_if_t* device = devices; device != nullptr; device = device->next)
65 {
66 if (device->name != nullptr && requested == device->name)
67 {
68 resolved = device->name;
69 break;
70 }
71
72 if (device->description != nullptr && requested == device->description)
73 {
74 resolved = device->name;
75 break;
76 }
77 }
78
79 pcap_freealldevs(devices);
80
81 return resolved;
82}
83
84} // namespace
85
86AsioEthernetSocket::EthernetPacketBackend::EthernetPacketBackend(asio::io_context& ioContext, const std::string& interface, std::uint16_t ethertype)
87 : ioContext(ioContext), ethertype(ethertype)
88#ifdef _WIN32
89 ,
90 packetWait(ioContext)
91#else
92 ,
93 packetWait(ioContext)
94#endif
95{
96 OpenCapture(interface);
98}
99
101{
102 asio::error_code error;
103
104 packetWait.cancel(error);
105 packetWait.close(error);
106
107 std::lock_guard captureLock(captureMutex);
108
109 if (capture != nullptr)
110 {
111 pcap_close(capture);
112 capture = nullptr;
113 }
114}
115
117{
118 if (interface.empty())
119 {
120 throw SocketError("Ethernet interface cannot be empty");
121 }
122
123 const std::string deviceName = ResolveCaptureDevice(interface);
124
125 char errorBuffer[PCAP_ERRBUF_SIZE]{};
126
127 capture = pcap_create(deviceName.c_str(), errorBuffer);
128
129 if (capture == nullptr)
130 {
131 throw SocketError("Failed to create packet capture on '" + interface + "': " + errorBuffer);
132 }
133
134 auto closeCapture = [this]()
135 {
136 if (capture != nullptr)
137 {
138 pcap_close(capture);
139 capture = nullptr;
140 }
141 };
142
143 if (pcap_set_snaplen(capture, static_cast<int>(MAX_ETHERNET_FRAME)) != 0)
144 {
145 const std::string message = pcap_geterr(capture);
146 closeCapture();
147 throw SocketError("Failed to configure packet capture snap length: " + message);
148 }
149
150 if (pcap_set_promisc(capture, 1) != 0)
151 {
152 const std::string message = pcap_geterr(capture);
153 closeCapture();
154 throw SocketError("Failed to configure packet capture promiscuous mode: " + message);
155 }
156
157 if (pcap_set_timeout(capture, kCaptureTimeoutMilliseconds) != 0)
158 {
159 const std::string message = pcap_geterr(capture);
160 closeCapture();
161 throw SocketError("Failed to configure packet capture timeout: " + message);
162 }
163
164 if (pcap_set_immediate_mode(capture, 1) != 0)
165 {
166 const std::string message = pcap_geterr(capture);
167 closeCapture();
168 throw SocketError("Failed to enable immediate packet delivery: " + message);
169 }
170
171 const int activateResult = pcap_activate(capture);
172
173 if (activateResult < 0)
174 {
175 try
176 {
177 ThrowPcapError("Failed to activate packet capture on '" + interface + "'", capture, activateResult);
178 }
179 catch (...)
180 {
181 closeCapture();
182 throw;
183 }
184 }
185
186 if (pcap_datalink(capture) != DLT_EN10MB)
187 {
188 const int dataLink = pcap_datalink(capture);
189 const std::string name = pcap_datalink_val_to_name(dataLink) != nullptr
190 ? pcap_datalink_val_to_name(dataLink)
191 : "unknown";
192
193 closeCapture();
194
195 throw SocketError("Interface '" + interface + "' does not expose Ethernet frames; libpcap data link type is " + name);
196 }
197
198 if (pcap_setnonblock(capture, 1, errorBuffer) != 0)
199 {
200 const std::string message = errorBuffer;
201 closeCapture();
202 throw SocketError("Failed to configure non-blocking packet capture: " + message);
203 }
204}
205
206void AsioEthernetSocket::EthernetPacketBackend::Send(const std::vector<std::uint8_t>& frame)
207{
208 if (frame.size() < kEthernetHeaderSize)
209 {
210 throw std::invalid_argument("Ethernet frame is shorter than the Ethernet header");
211 }
212
213 if (frame.size() > static_cast<std::size_t>(std::numeric_limits<int>::max()))
214 {
215 throw std::invalid_argument("Ethernet frame is too large");
216 }
217
218 std::lock_guard captureLock(captureMutex);
219
220 if (capture == nullptr)
221 {
222 throw SocketError("Ethernet packet backend is not open");
223 }
224
225 const int result = pcap_sendpacket(capture, frame.data(), static_cast<int>(frame.size()));
226
227 if (result != 0)
228 {
229 ThrowPcapError("Failed to send Ethernet frame", capture, result);
230 }
231}
232
233void AsioEthernetSocket::EthernetPacketBackend::SetTimeout(std::chrono::milliseconds newTimeout)
234{
235 std::lock_guard lock(timeoutMutex);
236
237 this->timeout = newTimeout > std::chrono::milliseconds::zero()
238 ? newTimeout
239 : std::chrono::milliseconds::zero();
240}
241
242std::vector<std::uint8_t>
244{
245 std::unique_lock receiveLock(receiveMutex);
246
247 std::chrono::milliseconds receiveTimeout;
248
249 {
250 std::lock_guard timeoutLock(timeoutMutex);
251 receiveTimeout = timeout;
252 }
253
254 const bool hasTimeout = receiveTimeout > std::chrono::milliseconds::zero();
255
256 const auto operation = std::make_shared<ReceiveOperation>(ioContext);
257
258 operation->pendingHandlers.store(hasTimeout ? 2U : 1U, std::memory_order_release);
259
260 std::future<std::vector<std::uint8_t>> completion = operation->completion.get_future();
261
262 AsyncWaitForPacket(operation);
263
264 if (hasTimeout)
265 {
266 operation->timeoutTimer.expires_after(receiveTimeout);
267
268 operation->timeoutTimer.async_wait([this, operation](const asio::error_code& error)
269 {
270 HandleTimeout(operation, error);
271 });
272 }
273
274 return completion.get();
275}
276
277void AsioEthernetSocket::EthernetPacketBackend::HandlePacketReady(const std::shared_ptr<ReceiveOperation>& operation, const asio::error_code& error)
278{
279 if (error)
280 {
281 if (error != asio::error::operation_aborted)
282 {
283 FailOperation(operation, std::make_exception_ptr(SocketError("Ethernet packet wait failed: " + error.message())));
284 }
285
286 CompleteHandler(operation);
287 return;
288 }
289
290 if (operation->state.load(std::memory_order_acquire) != ReceiveOperation::State::Pending)
291 {
292 CompleteHandler(operation);
293 return;
294 }
295
296 std::vector<std::uint8_t> frame;
297
298 const int result = CaptureNextFrame(frame);
299
300 if (result == 1)
301 {
302 if (MatchesEtherType(frame))
303 {
304 ReceiveOperation::State expected = ReceiveOperation::State::Pending;
305
306 if (operation->state.compare_exchange_strong(expected, ReceiveOperation::State::Received, std::memory_order_acq_rel))
307 {
308 {
309 std::lock_guard lock(operation->resultMutex);
310 operation->frame = std::move(frame);
311 }
312
313 asio::error_code cancelError;
314 operation->timeoutTimer.cancel(cancelError);
315 CancelPacketWait(operation, cancelError);
316 }
317 }
318 else
319 {
320 RearmPacketWait(operation);
321 }
322 }
323 else if (result == 0)
324 {
325 RearmPacketWait(operation);
326 }
327 else if (result == -2)
328 {
329 FailOperation(operation, std::make_exception_ptr(SocketError("Ethernet packet capture was terminated")));
330 }
331 else
332 {
333 std::string message = "Failed to receive Ethernet frame";
334
335 {
336 std::lock_guard captureLock(captureMutex);
337
338 if (capture != nullptr)
339 {
340 const char* detail = pcap_geterr(capture);
341
342 if (detail != nullptr && *detail != '\0')
343 {
344 message += ": ";
345 message += detail;
346 }
347 }
348 }
349
350 FailOperation(operation, std::make_exception_ptr(SocketError(message)));
351 }
352
353 CompleteHandler(operation);
354}
355
356void AsioEthernetSocket::EthernetPacketBackend::HandleTimeout(const std::shared_ptr<ReceiveOperation>& operation, const asio::error_code& error)
357{
358 if (!error)
359 {
360 ReceiveOperation::State expected = ReceiveOperation::State::Pending;
361
362 if (operation->state.compare_exchange_strong(expected, ReceiveOperation::State::TimedOut, std::memory_order_acq_rel))
363 {
364 asio::error_code cancelError;
365 CancelPacketWait(operation, cancelError);
366 }
367 }
368
369 CompleteHandler(operation);
370}
371
372void AsioEthernetSocket::EthernetPacketBackend::RearmPacketWait(const std::shared_ptr<ReceiveOperation>& operation)
373{
374 if (operation->state.load(std::memory_order_acquire) != ReceiveOperation::State::Pending)
375 {
376 return;
377 }
378
379 operation->pendingHandlers.fetch_add(1U, std::memory_order_acq_rel);
380
381 if (operation->state.load(std::memory_order_acquire) != ReceiveOperation::State::Pending)
382 {
383 operation->pendingHandlers.fetch_sub(1U, std::memory_order_acq_rel);
384
385 return;
386 }
387
388 try
389 {
390 AsyncWaitForPacket(operation);
391 }
392 catch (...)
393 {
394 operation->pendingHandlers.fetch_sub(1U, std::memory_order_acq_rel);
395
396 FailOperation(operation, std::current_exception());
397 }
398}
399
400void AsioEthernetSocket::EthernetPacketBackend::FailOperation(const std::shared_ptr<ReceiveOperation>& operation, std::exception_ptr error)
401{
402 ReceiveOperation::State expected = ReceiveOperation::State::Pending;
403
404 if (!operation->state.compare_exchange_strong(expected, ReceiveOperation::State::Failed, std::memory_order_acq_rel))
405 {
406 return;
407 }
408
409 {
410 std::lock_guard lock(operation->resultMutex);
411 operation->error = std::move(error);
412 }
413
414 asio::error_code cancelError;
415
416 CancelPacketWait(operation, cancelError);
417 operation->timeoutTimer.cancel(cancelError);
418}
419
420void AsioEthernetSocket::EthernetPacketBackend::CompleteHandler(const std::shared_ptr<ReceiveOperation>& operation)
421{
422 if (operation->pendingHandlers.fetch_sub(1U, std::memory_order_acq_rel) != 1U)
423 {
424 return;
425 }
426
427 const ReceiveOperation::State state = operation->state.load(std::memory_order_acquire);
428
429 std::vector<std::uint8_t> frame;
430 std::exception_ptr error;
431
432 {
433 std::lock_guard lock(operation->resultMutex);
434
435 frame = std::move(operation->frame);
436 error = operation->error;
437 }
438
439 if (state == ReceiveOperation::State::Failed)
440 {
441 operation->completion.set_exception(error);
442 }
443 else if (state == ReceiveOperation::State::TimedOut)
444 {
445 operation->completion.set_value({});
446 }
447 else
448 {
449 operation->completion.set_value(std::move(frame));
450 }
451}
452
454{
455 std::lock_guard captureLock(captureMutex);
456
457 if (capture == nullptr)
458 {
459 throw SocketError("Ethernet packet backend is not open");
460 }
461
462 pcap_pkthdr* header = nullptr;
463 const u_char* data = nullptr;
464
465 const int result = pcap_next_ex(capture, &header, &data);
466
467 if (result == 1 && header != nullptr && data != nullptr)
468 {
469 frame.assign(data, data + header->caplen);
470 }
471
472 return result;
473}
474
475bool AsioEthernetSocket::EthernetPacketBackend::MatchesEtherType(const std::vector<std::uint8_t>& frame) const
476{
477 if (ethertype == 0U)
478 {
479 return true;
480 }
481
482 if (frame.size() < kEthernetHeaderSize)
483 {
484 return false;
485 }
486
487 const std::size_t etherTypeOffset = SkipVlanTags(frame);
488
489 if (etherTypeOffset + sizeof(std::uint16_t) > frame.size())
490 {
491 return false;
492 }
493
494 const auto frameType = static_cast<std::uint16_t>((static_cast<std::uint16_t>(frame[etherTypeOffset]) << 8U) |
495 static_cast<std::uint16_t>(frame[etherTypeOffset + 1U]));
496
497 return frameType == ethertype;
498}
499
500} // namespace profinet
void HandleTimeout(const std::shared_ptr< ReceiveOperation > &operation, const asio::error_code &error)
Handle application receive timeout.
void SetTimeout(std::chrono::milliseconds newTimeout)
Configure the receive timeout.
void RearmPacketWait(const std::shared_ptr< ReceiveOperation > &operation)
Rearm packet readiness for another asynchronous operation.
EthernetPacketBackend(asio::io_context &ioContext, const std::string &interface, std::uint16_t ethertype)
Construct the packet backend.
void FailOperation(const std::shared_ptr< ReceiveOperation > &operation, std::exception_ptr error)
Mark a receive operation as failed.
std::vector< std::uint8_t > Recv()
Receive a complete Ethernet frame.
bool MatchesEtherType(const std::vector< std::uint8_t > &frame) const
Check whether an Ethernet frame matches the configured EtherType.
void OpenCapture(const std::string &interface)
Initialize the libpcap packet capture handle.
void Send(const std::vector< std::uint8_t > &frame)
Send one complete Ethernet frame.
int CaptureNextFrame(std::vector< std::uint8_t > &frame)
Read one packet from libpcap without blocking.
void CompleteHandler(const std::shared_ptr< ReceiveOperation > &operation)
Complete one asynchronous handler.
void HandlePacketReady(const std::shared_ptr< ReceiveOperation > &operation, const asio::error_code &error)
Handle packet-source readiness.
void InitializePacketWait()
Connect the packet source to the Asio readiness mechanism.
asio::io_context & ioContext
Asio execution context supplied by the caller.
Socket operation error.
Definition exceptions.h:663
Private Ethernet packet backend used by AsioEthernetSocket.
Declares exceptions and error types used by the PROFINET IO controller stack.
constexpr std::size_t MAX_ETHERNET_FRAME
Maximum PROFINET Ethernet frame size in bytes (including VLAN tag headroom).
Definition util.h:28
std::size_t SkipVlanTags(std::span< const std::uint8_t > frame) noexcept
Return the byte offset of the real EtherType in a raw Ethernet frame.
Definition util.cpp:70
asio::error_code error
std::promise< void > completion
Bytes frame
std::unique_ptr< asio::io_context > ioContext
Shared ASIO context.
Declares Linux Ethernet, addressing, timing, and utility helpers used by the PROFINET stack.