SatCat5
ip_icmp.cc
1 // Copyright 2021-2025 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
5 
6 #include <satcat5/ip_address.h>
7 #include <satcat5/ip_dispatch.h>
8 #include <satcat5/ip_icmp.h>
9 #include <satcat5/log.h>
10 #include <satcat5/timeref.h>
11 
12 // Enable more detailed ICMP error messages?
13 // Disabling this feature can save some code-size.
14 #ifndef SATCAT5_ICMP_DETAIL
15 #define SATCAT5_ICMP_DETAIL 1 // Level of detail (0/1/2)
16 #endif
17 
18 // Default rate limit for ICMP error logging (in milliseconds).
19 #ifndef SATCAT5_ICMP_LOG_COOLDOWN
20 #define SATCAT5_ICMP_LOG_COOLDOWN 500
21 #endif
22 
24 using satcat5::net::Type;
25 namespace ip = satcat5::ip;
26 namespace log = satcat5::log;
27 
28 // Dispatch type codes.
29 static constexpr Type TYPE_ICMP = Type(ip::PROTO_ICMP);
30 
31 // ICMP timestamps use MSB to indicate format:
32 // '0' = Milliseconds since midnight
33 // '1' = Any other format
34 static constexpr u32 TIMESTAMP_ARB = (1u << 31);
35 
36 // Length of various message types.
37 static constexpr unsigned ECHO_WORDS = 4;
38 static constexpr unsigned TIME_WORDS = 10;
39 
40 // Translate ICMP type-code to a human-readable error message, if applicable.
41 static inline const char* code2msg(u16 code) {
42  if (SATCAT5_ICMP_DETAIL >= 2) {
43  // Full error for less-common messages.
44  if (code == ip::ICMP_FRAG_REQUIRED)
45  return "Fragmentation required but DF set";
46  if (code == ip::ICMP_SRC_ROUTE_FAILED)
47  return "Source route failed";
48  if (code == ip::ICMP_DST_NET_UNKNOWN)
49  return "Destination network unknown";
50  if (code == ip::ICMP_DST_HOST_UNKNOWN)
51  return "Destination host unknown";
52  if (code == ip::ICMP_SRC_HOST_ISOLATED)
53  return "Source host isolated";
54  if (code == ip::ICMP_NET_PROHIBITED)
55  return "Network administratively prohibited";
56  if (code == ip::ICMP_HOST_PROHIBITED)
57  return "Host administratively prohibited";
58  if (code == ip::ICMP_TOS_NET)
59  return "Network unreachable for ToS";
60  if (code == ip::ICMP_TOS_HOST)
61  return "Host unreachable for ToS";
62  if (code == ip::ICMP_COMM_PROHIBITED)
63  return "Communication administratively prohibited";
64  if (code == ip::ICMP_HOST_PRECEDENCE)
65  return "Host precedence violation";
66  if (code == ip::ICMP_PRECEDENCE_CUTOFF)
67  return "Precedence cutoff in effect";
68  if (code == ip::ICMP_FRAG_TIMEOUT)
69  return "Fragment reassembly time exceeded";
70  if (code == ip::ICMP_IP_HDR_POINTER)
71  return "IP Header: Pointer error";
72  if (code == ip::ICMP_IP_HDR_OPTION)
73  return "IP Header: Missing required option";
74  if (code == ip::ICMP_IP_HDR_LENGTH)
75  return "IP Header: Bad length";
76  }
77 
78  if (SATCAT5_ICMP_DETAIL >= 1) {
79  // Full error for common messages.
80  if (code == ip::ICMP_UNREACHABLE_NET)
81  return "Destination network unreachable";
82  if (code == ip::ICMP_UNREACHABLE_HOST)
83  return "Destination host unreachable";
84  if (code == ip::ICMP_UNREACHABLE_PROTO)
85  return "Destination protocol unreachable";
86  if (code == ip::ICMP_UNREACHABLE_PORT)
87  return "Destination port unreachable";
88  if (code == ip::ICMP_TTL_EXPIRED)
89  return "TTL expired in transit";
90  }
91 
92  // Catch-all for broad error categories.
93  u16 type = code & ip::ICMP_TYPE_MASK;
94  if (type == ip::ICMP_TYPE_UNREACHABLE)
95  return "Destination unreachable";
96  if (type == ip::ICMP_TYPE_TIME_EXCEED)
97  return "Time exceeded";
98  if (type == ip::ICMP_TYPE_BAD_IP_HDR)
99  return "IP header error";
100 
101  // Ignore all other messages.
102  return 0;
103 }
104 
105 ProtoIcmp::ProtoIcmp(ip::Dispatch* iface)
106  : satcat5::net::Protocol(TYPE_ICMP)
107  , m_iface(iface)
108  , m_wait_msec(SATCAT5_ICMP_LOG_COOLDOWN)
109 {
110  m_iface->add(this);
111 }
112 
113 #if SATCAT5_ALLOW_DELETION
114 ProtoIcmp::~ProtoIcmp()
115 {
116  m_iface->remove(this);
117 }
118 #endif
119 
120 bool ProtoIcmp::send_error(u16 type, satcat5::io::Readable* src, u32 arg)
121 {
122  // ICMP error messages always include:
123  // u16 Typecode
124  // u16 Checksum
125  // u32 Argument / unused (varies)
126  // Varies IPv4 header (max 30 x uint16)
127  // 4 x u16 First 8 bytes from datagram
128  // This gives a total worst-case length of 38 x uint16.
129  u16 buff[ip::HDR_MAX_SHORTS + 8];
130 
131  // Get header from the packet that triggered this error.
132  const ip::Header& hdr = m_iface->reply_hdr();
133 
134  // Start populating the message fields:
135  u16* ptr = buff;
136  (*ptr++) = type; // Message type
137  (*ptr++) = 0; // Placeholder for checksum
138  (*ptr++) = (u16)(arg >> 16); // Special argument (varies)
139  (*ptr++) = (u16)(arg >> 0); // Special argument (varies)
140  for (unsigned a = 0 ; a < 2 * hdr.ihl() ; ++a)
141  (*ptr++) = hdr.data[a]; // Copy the original IPv4 header
142  for (unsigned a = 0 ; a < 4 ; ++a)
143  (*ptr++) = src->read_u16(); // Copy first 8 bytes of user data
144 
145  // Open a stream and send the error message.
146  unsigned wcount = ptr - buff; // Length (16-bit words)
147  satcat5::io::Writeable* wr = m_iface->open_reply(TYPE_ICMP, 2*wcount);
148  return write_icmp(wr, wcount, buff);
149 }
150 
152 {
153  // Embed current time in the request packet.
154  u32 now = SATCAT5_CLOCK->raw();
155 
156  // Formulate the ICMP echo request:
157  u16 buff[ECHO_WORDS];
158  buff[0] = ip::ICMP_ECHO_REQUEST; // Message type
159  buff[1] = 0; // Placeholder for checksum
160  buff[2] = (u16)(now >> 16); // Timestamp (MSBs)
161  buff[3] = (u16)(now >> 0); // Timestamp (LSBs)
162 
163  // Open a stream and send the echo-request message.
164  satcat5::io::Writeable* wr = dst.open_write(2*ECHO_WORDS);
165  return write_icmp(wr, ECHO_WORDS, buff);
166 }
167 
169 {
170  // "Originate" timestamp uses the arbitrary-units flag.
171  u32 now = SATCAT5_CLOCK->raw() | TIMESTAMP_ARB;
172 
173  // Formulate the timestamp request:
174  u16 buff[TIME_WORDS];
175  buff[0] = ip::ICMP_TIME_REQUEST; // Message type
176  buff[1] = 0; // Placeholder for checksum
177  buff[2] = 0xDEAD; // Identifier (unused)
178  buff[3] = 0xBEEF; // Sequence (unused)
179  buff[4] = (u16)(now >> 16); // Timestamp (MSBs)
180  buff[5] = (u16)(now >> 0); // Timestamp (LSBs)
181  buff[6] = 0; // Zero-pad placeholders
182  buff[7] = 0;
183  buff[8] = 0;
184  buff[9] = 0;
185 
186  // Open a stream and send the echo-request message.
187  satcat5::io::Writeable* wr = dst.open_write(2*TIME_WORDS);
188  return write_icmp(wr, TIME_WORDS, buff);
189 }
190 
192 {
193  // Maximum reply length = N words (arbitrary cutoff).
194  // (If an echo attempts to exceed this limit, ignore it.)
195  static const unsigned MAX_REPLY = 32;
196  static const unsigned MAX_ECHO = MAX_REPLY - 2;
197  u16 buff[MAX_REPLY];
198 
199  // Ignore anything that's too short to be a valid packet.
200  if (src.get_read_ready() < 8) return;
201 
202  // Read the common header.
203  u16 code = src.read_u16(); // Message type + code
204  src.read_u16(); // Checksum (discard)
205  unsigned wlen = src.get_read_ready() / 2;
206 
207  // Handle each supported message type:
208  u16 type = code & ip::ICMP_TYPE_MASK;
209  auto src_ip = m_iface->reply_ip();
210  if (code == ip::ICMP_ECHO_REPLY) {
211  // Ping response: Extract the timestamp we inserted earlier.
212  satcat5::util::TimeVal tref = {SATCAT5_CLOCK, src.read_u32()};
213  u32 elapsed = tref.elapsed_usec();
214  // Notify listeners of the elapsed round-trip time.
215  satcat5::ip::PingListener* item = m_listeners.head();
216  while (item) {
217  item->ping_event(m_iface->reply_ip(), elapsed);
218  item = m_listeners.next(item);
219  }
220  } else if (code == ip::ICMP_ECHO_REQUEST && wlen <= MAX_ECHO) {
221  // Ping request: Copy data from echo request
222  buff[0] = ip::ICMP_ECHO_REPLY; // Message type
223  buff[1] = 0; // Placeholder for checksum
224  for (unsigned a = 0 ; a < wlen ; ++a)
225  buff[a+2] = src.read_u16(); // Copy from echo request
226  // Open a stream and send the echo-request message.
227  unsigned echo_len = wlen + 2; // Echo data + header
228  satcat5::io::Writeable* wr = m_iface->open_reply(TYPE_ICMP, 2*echo_len);
229  write_icmp(wr, echo_len, buff);
230  } else if (type == ip::ICMP_TYPE_REDIRECT && wlen >= 12) {
231  // Redirect: Parse message and forward to ARP handler.
232  ip::Addr dstaddr, gateway;
233  src.read_obj(gateway); // First field is the new gateway
234  src.read_consume(16); // Skip ahead to the destination
235  src.read_obj(dstaddr); // Read DST from IPv4 header
236  m_iface->arp()->gateway_change(dstaddr, gateway);
237  } else if (code == ip::ICMP_TIME_REPLY && wlen >= 8) {
238  // Timestamp response: Log but take no further action.
239  src.read_consume(12); // Skip unused information
240  u32 stamp = src.read_u32(); // Read transmit timestamp
241  log::Log(log::INFO, "Timestamp response").write(stamp);
242  } else if (code == ip::ICMP_TIME_REQUEST && wlen >= 8) {
243  // Timestamp request: Reply uses the arbitrary-units flag.
244  u32 now = SATCAT5_CLOCK->raw() | TIMESTAMP_ARB;
245  // Construct and send the timestamp response (10 words).
246  buff[0] = ip::ICMP_TIME_REPLY; // Message type
247  buff[1] = 0; // Placeholder for checksum
248  buff[2] = src.read_u16(); // Echo ID
249  buff[3] = src.read_u16(); // Echo Seq
250  buff[4] = src.read_u16(); // Echo timestamp (MSBs)
251  buff[5] = src.read_u16(); // Echo timestamp (LSBs)
252  buff[6] = (u16)(now >> 16); // Receive timestamp
253  buff[7] = (u16)(now >> 0); // Receive timestamp
254  buff[8] = (u16)(now >> 16); // Transmit timestamp
255  buff[9] = (u16)(now >> 0); // Transmit timestamp
256  // Open a stream and send the timestamp-reply message.
257  satcat5::io::Writeable* wr = m_iface->open_reply(TYPE_ICMP, 2*TIME_WORDS);
258  write_icmp(wr, TIME_WORDS, buff);
259  } else if (timer_remaining() == 0) {
260  // Log the error message, if any (varying verbosity options)
261  const char* msg = code2msg(code);
262  if (msg) {log::Log(log::WARNING, msg).write(src_ip);}
263  // Set rate-limiting timer before logging another message?
264  if (m_wait_msec) timer_once(m_wait_msec);
265  }
266 }
267 
269  // No action required.
270 }
271 
272 bool ProtoIcmp::write_icmp(
274  unsigned wcount, u16* data)
275 {
276  // Sanity check that the output stream is OK.
277  if (!wr) return false;
278 
279  // ICMP checksum is always placed at the same offset.
280  data[1] = ip::checksum(wcount, data);
281 
282  // Write frame contents to the designated buffer.
283  for (unsigned a = 0 ; a < wcount ; ++a)
284  wr->write_u16(data[a]);
285  return wr->write_finalize();
286 }
void gateway_change(const satcat5::ip::Addr &dstaddr, const satcat5::ip::Addr &gateway)
Notify all listeners of a change in gateway configuration.
Definition: eth_arp.cc:124
Limited read of next N bytes.
Definition: io_readable.h:255
bool read_consume(unsigned nbytes) override
Read and discard 0 or more bytes.
Definition: io_readable.cc:307
unsigned get_read_ready() const override
How many bytes can be read without blocking?
Definition: io_readable.cc:293
Abstract API for reading byte-streams and packets.
Definition: io_readable.h:68
bool read_obj(T &t)
Templated wrapper for any object with the following method: bool read_from(satcat5::io::Readable* rd)...
Definition: io_readable.h:140
Abstract API for writing byte-streams and packets.
Definition: io_writeable.h:24
virtual bool write_finalize()
Mark end of frame and release temporary working data.
Connection metadata for an IPv4 address.
Definition: ip_address.h:38
satcat5::io::Writeable * open_write(unsigned len) override
Open a new frame to the designated address and type.
Definition: ip_address.cc:124
Protocol handler and dispatch unit for Internet Protocol v4 (IPv4).
Definition: ip_dispatch.h:43
satcat5::io::Writeable * open_reply(const satcat5::net::Type &type, unsigned len) override
Get Writeable object for reply to the last received datagram.
Definition: ip_dispatch.cc:51
Callback API for handling "ping" reponses.
Definition: ip_icmp.h:62
virtual void ping_event(const satcat5::ip::Addr &from, u32 elapsed_usec)=0
Callback method when a ping response is received.
Protocol handler for the Internet Control Message Protocol (ICMP).
Definition: ip_icmp.h:81
bool send_timereq(satcat5::ip::Address &dst)
Initiate a timestamp request.
Definition: ip_icmp.cc:168
void timer_event() override
Child class MUST override this method.
Definition: ip_icmp.cc:268
bool send_error(u16 type, satcat5::io::Readable *src, uint32_t arg=0)
Send a specific error message.
Definition: ip_icmp.cc:120
bool send_ping(satcat5::ip::Address &dst)
Initiate a ping (Echo request = Type 8.0).
Definition: ip_icmp.cc:151
void frame_rcvd(satcat5::io::LimitedRead &src) override
Dispatch calls frame_rcvd(...) for each incoming frame with with a matching net::Type value.
Definition: ip_icmp.cc:191
The Log class creates and formats one log message.
Definition: log.h:195
Log & write(const char *str)
Formatting methods for various data types.
Definition: log.cc:198
void add(satcat5::net::Protocol *proto)
Register a Protocol object.
Definition: net_dispatch.h:55
void remove(satcat5::net::Protocol *proto)
Unregister a Protocol object.
Definition: net_dispatch.h:59
unsigned timer_remaining() const
Accessor for time to next event, if one is set.
Definition: polling.h:229
void timer_once(unsigned msec)
Configure a one-time notification after X milliseconds.
Definition: polling.cc:316
T * next(const T *item) const
Fetch pointer to the next item.
Definition: list.h:261
Diagnostic logging to UART and/or Ethernet ports.
IPv4 address is a 32-bit unsigned integer.
Definition: ip_core.h:15
Structure for holding an IPv4 Header, including options.
Definition: ip_core.h:180
constexpr unsigned ihl() const
< Header length (4-byte words)
Definition: ip_core.h:192
u16 data[HDR_MAX_SHORTS]
Raw access to the underlying header contents.
Definition: ip_core.h:182
Multipurpose filter for matching fields in network packets.
Definition: net_type.h:38
Timestamp for measuring elapsed time.
Definition: timeref.h:48
unsigned elapsed_usec() const
Elapsed time in microseconds.
Definition: timeref.cc:25
TimeRef and TimeVal define the API for monotonic timers.