SatCat5
igmp_client.cc
1 // Copyright 2025 The Aerospace Corporation.
3 // This file is a part of SatCat5, licensed under CERN-OHL-W v2 or later.
5 
6 #include <satcat5/igmp_client.h>
7 #include <satcat5/ip_dispatch.h>
8 #include <satcat5/utils.h>
9 
12 using satcat5::ip::Addr;
13 using satcat5::ip::ADDR_NONE;
17 
18 // Filter "protocol" field for incoming IPv4 packets.
19 static constexpr satcat5::net::Type
20  TYPE_IGMP(satcat5::ip::PROTO_IGMP);
21 
22 // Set the polling interval for response timers.
23 static constexpr u32 TIMER_INTERVAL_MSEC = 10;
24 
25 // Formulate and send an IGMP message.
27  satcat5::ip::Dispatch* iface,
28  const satcat5::ip::Addr& dst,
29  const unsigned wcount, u16* data)
30 {
31  // TODO: Why is this needed? Possible compiler bug?
32  // Adding this line prevents a linker error on Microblaze.
33  // "collect2.exe: error: ld returned 5 exit status"
34  if (!SATCAT5_IGMP_ENABLE) return false;
35 
36  // Sanity check on array bounds.
37  if (wcount < MIN_LEN_SHORTS || !data) return false;
38 
39  // In-place calculation of the message checksum.
40  // (Same location for all IGMP message types and all versions.)
41  data[1] = satcat5::ip::checksum(wcount, data);
42 
43  // Write the Ethernet header.
44  satcat5::io::Writeable* wr = iface->iface()->open_write(
45  satcat5::eth::MACADDR_BROADCAST, satcat5::eth::ETYPE_IPV4);
46  if (!wr) return false;
47 
48  // Write the IP header with TTL=1 and the "router alert" option.
49  satcat5::ip::Header hdr = iface->next_header(
50  satcat5::ip::PROTO_IGMP, dst, 2*wcount, 1);
51  hdr.option_alert();
52  hdr.write_to(wr);
53 
54  // Write the message contents.
55  for (unsigned a = 0 ; a < wcount ; ++a)
56  wr->write_u16(data[a]);
57  return wr->write_finalize();
58 }
59 
60 u32 satcat5::igmp::decode_maxdly(u16 hdr, bool v3) {
61  hdr &= MASK_MAXLEN;
62  if (!hdr) return 10000; // IGMPv1
63  if (hdr < 128 || !v3) return 100*hdr; // IGMPv2 or IGMPv3-fixed
64  u16 mant = (hdr & 0x0F) + 16;
65  u16 expn = (hdr & 0x70) >> 4;
66  return u32(100 * mant) << (expn + 3); // IGMPv3-float
67 }
68 
69 #if SATCAT5_IGMP_ENABLE
70 
71 Client::Client(satcat5::ip::Dispatch* iface)
72  : Protocol(TYPE_IGMP) // Accept IGMP packets (proto = 0x02)
73  , m_iface(iface) // Link to parent interface
74  , m_reply_timer(0)
75  , m_version(3) // Assume v3, downgrade as needed
76 {
77  m_iface->add(this);
78  timer_every(TIMER_INTERVAL_MSEC);
79 }
80 
81 Client::~Client() {
82  #if SATCAT5_ALLOW_DELETION
83  m_iface->remove(this);
84  #endif
85 }
86 
87 void Client::join(Address& obj, const Addr& addr) {
88  // Leave existing group, if we haven't already.
89  leave(obj);
90  // Sanity check on new group address.
91  if (addr.is_broadcast() || !addr.is_multicast()) return;
92  // Add the group to the list of active objects.
93  obj.m_igmp_group = addr;
94  m_groups.add_safe(&obj);
95  // Send two "join" messages: one now, one after a short delay.
96  if (m_version > 1) {
97  send_report(addr);
98  obj.m_igmp_timer = prng.next(1, 1000);
99  }
100 }
101 
102 void Client::leave(Address& obj) {
103  // Sanity check on current state.
104  if (obj.m_igmp_group == ADDR_NONE) return;
105  // If applicable, send a "leave" message.
106  if (m_version > 1) send_leave(obj.m_igmp_group);
107  // Remove the group from the list of active objects.
108  m_groups.remove(&obj);
109  obj.m_igmp_group = ADDR_NONE;
110 }
111 
113  // Sanity check the incoming packet length.
114  if (src.get_read_ready() > MAX_LEN_BYTES ||
115  src.get_read_ready() < MIN_LEN_BYTES) return;
116 
117  // Read packet contents and validate the checksum.
118  u16 rcvd[MAX_LEN_SHORTS];
119  unsigned wcount = 0;
120  while (src.get_read_ready())
121  rcvd[wcount++] = src.read_u16();
122  u16 chk = satcat5::ip::checksum(wcount, rcvd);
123  if (chk) return; // Expect total = zero.
124 
125  // Decode header fields.
126  u16 type = rcvd[0] & MASK_TYPE;
127  u16 mdly = rcvd[0] & MASK_MAXLEN;
128  Addr addr(rcvd[2], rcvd[3]);
129 
130  // Sort by type header...
131  if (type == TYPE_QUERY) {
132  // Autodetect the query version (RFC9776, Section 7.1).
133  // There should only be one active "Querier" per network.
134  if (mdly == 0) {
135  m_version = 1;
136  } else if (wcount == 4) {
137  m_version = 2;
138  } else {
139  m_version = 3;
140  }
141  u32 max_dly = decode_maxdly(mdly, wcount > 8);
142  // Schedule response(s) with randomized delays.
143  if (addr != ADDR_NONE) {
144  // Query for a specific address.
145  auto match = find_match(addr);
146  if (match) set_timer(match->m_igmp_timer, max_dly);
147  } else if (m_version == 3) {
148  // General query V3: Set timer for all-in-one response.
149  set_timer(m_reply_timer, max_dly);
150  } else {
151  // General query V1 or V2: Set timer for each response.
152  Address* item = m_groups.head();
153  while (item) {
154  set_timer(item->m_igmp_timer, max_dly);
155  item = m_groups.next(item);
156  }
157  }
158  } else if (type == TYPE_REPORT_V1 || type == TYPE_REPORT_V2) {
159  // Suppress duplicates if we would have responded to the same address.
160  // (This behavior was deprecated in IGMPv3, see RFC9776 Appendix A.)
161  auto match = find_match(addr); // GCOVR_EXCL_LINE
162  if (match) match->m_igmp_timer = 0; // GCOVR_EXCL_LINE
163  }
164 }
165 
167  // Decrement the global countdown timer (IGMPv3 only).
168  if (countdown(m_reply_timer, TIMER_INTERVAL_MSEC)) {
169  send_report(ADDR_NONE);
170  }
171  // Decrement each of the per-group timers.
172  Address* item = m_groups.head();
173  while (item) {
174  if (countdown(item->m_igmp_timer, TIMER_INTERVAL_MSEC)) {
175  send_report(item->m_igmp_group);
176  }
177  item = m_groups.next(item);
178  }
179 }
180 
181 Address* Client::find_match(const Addr& group) const {
182  Address* item = m_groups.head();
183  while (item && item->m_igmp_group != group) {
184  item = m_groups.next(item);
185  }
186  return item;
187 }
188 
189 void Client::send_leave(const Addr& group) {
190  u16 msg[MIN_LEN_SHORTS];
191  unsigned wcount = 0;
192  msg[wcount++] = TYPE_LEAVE_V2;
193  msg[wcount++] = 0; // Placeholder for checksum
194  msg[wcount++] = u16(group.value >> 16);
195  msg[wcount++] = u16(group.value >> 0);
196  igmp_send(m_iface, DST_ALL_ROUTERS, wcount, msg);
197 }
198 
199 void Client::send_report(const Addr& group) {
200  u16 msg[MAX_LEN_SHORTS];
201  unsigned wcount = 0;
202  if (m_version == 1) {
203  // IGMPv1 report with a single address.
204  msg[wcount++] = TYPE_REPORT_V1;
205  msg[wcount++] = 0; // Placeholder for checksum
206  msg[wcount++] = u16(group.value >> 16);
207  msg[wcount++] = u16(group.value >> 0);
208  igmp_send(m_iface, group, wcount, msg);
209  } else if (m_version == 2) {
210  // IGMPv2 report with a single address.
211  msg[wcount++] = TYPE_REPORT_V2;
212  msg[wcount++] = 0; // Placeholder for checksum
213  msg[wcount++] = u16(group.value >> 16);
214  msg[wcount++] = u16(group.value >> 0);
215  igmp_send(m_iface, group, wcount, msg);
216  } else if (!m_groups.is_empty()) {
217  // IGMPv3 report with all desired addresses.
218  unsigned len = min_unsigned(MAX_V3_RECORDS, m_groups.len());
219  msg[wcount++] = TYPE_REPORT_V3;
220  msg[wcount++] = 0; // Placeholder for checksum
221  msg[wcount++] = 0; // Flags (unused)
222  msg[wcount++] = u16(len);
223  // Append a record for each group.
224  Address* item = m_groups.head();
225  for (unsigned a = 0 ; item && a < len ; ++a) {
226  msg[wcount++] = RECORD_MODE_EXCL; // Record type + aux len
227  msg[wcount++] = 0; // No source-address filters
228  msg[wcount++] = u16(item->m_igmp_group.value >> 16);
229  msg[wcount++] = u16(item->m_igmp_group.value >> 0);
230  item = m_groups.next(item); // Traverse linked list
231  }
232  igmp_send(m_iface, DST_IGMPV3_REPORT, wcount, msg);
233  }
234 }
235 
236 void Client::set_timer(u32& timer, u32 max_msec) {
237  // Randomize the delay amount, then keep the shortest of new and old.
238  u32 new_dly = prng.next(1, max_msec);
239  if (new_dly < timer || !timer) timer = new_dly;
240 }
241 
242 #else // SATCAT5_IGMP_ENABLE
243 
244 // If IGMP is disabled, provide empty stubs for all public methods.
246 Client::~Client() {}
247 void Client::join(Address& obj, const Addr& addr) {}
248 void Client::leave(Address& obj) {}
249 
250 #endif // SATCAT5_IGMP_ENABLE
satcat5::io::Writeable * open_write(const satcat5::eth::MacAddr &dst, const satcat5::eth::MacType &type, satcat5::eth::VlanTag vtag=satcat5::eth::VTAG_NONE)
Send a frame to the designated Ethernet address/VLAN.
Definition: eth_dispatch.cc:74
Host/Client for the Internet Group Management Protocol (IGMP).
Definition: igmp_client.h:108
Client(satcat5::ip::Dispatch *iface)
Link this handler to an IPv4 network interface.
Definition: igmp_client.cc:71
void join(satcat5::igmp::Address &obj, const satcat5::ip::Addr &addr)
Join a multicast group.
Definition: igmp_client.cc:87
void timer_event() override
Child class MUST override this method.
Definition: igmp_client.cc:166
void frame_rcvd(satcat5::io::LimitedRead &src) override
Dispatch calls frame_rcvd(...) for each incoming frame with with a matching net::Type value.
Definition: igmp_client.cc:112
void leave(satcat5::igmp::Address &obj)
Leave a multicast group.
Definition: igmp_client.cc:102
Limited read of next N bytes.
Definition: io_readable.h:255
unsigned get_read_ready() const override
How many bytes can be read without blocking?
Definition: io_readable.cc:293
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.
Protocol handler and dispatch unit for Internet Protocol v4 (IPv4).
Definition: ip_dispatch.h:43
satcat5::ip::Header next_header(u8 protocol, const satcat5::ip::Addr &dst, unsigned inner_bytes, u8 ttl=SATCAT5_IP_TTL)
Create a basic IPv4 header with the specified information.
Definition: ip_dispatch.cc:79
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
void timer_every(unsigned msec)
Configure a repeating notification every X milliseconds.
Definition: polling.cc:321
unsigned len() const
Traverse the linked list to count its length.
Definition: list.h:257
T * next(const T *item) const
Fetch pointer to the next item.
Definition: list.h:261
void add_safe(T *item)
Check if list already contains item before adding.
Definition: list.h:231
bool is_empty() const
Is this list empty?
Definition: list.h:253
void remove(T *item)
Remove the designated item from the list.
Definition: list.h:277
u32 next()
Range [0..2^32)
Definition: utils.cc:194
Client-side implementation of the Internet Group Management Protocol (IGMP)
constexpr unsigned MIN_LEN_SHORTS
Set upper and lower bounds for the length of an IGMP message.
Definition: igmp_client.h:56
constexpr unsigned MAX_LEN_SHORTS
Set upper and lower bounds for the length of an IGMP message.
Definition: igmp_client.h:58
constexpr u16 TYPE_REPORT_V3
Reply to IGMPv3 query.
Definition: igmp_client.h:71
constexpr unsigned MIN_LEN_BYTES
Set upper and lower bounds for the length of an IGMP message.
Definition: igmp_client.h:57
constexpr unsigned MAX_LEN_BYTES
Set upper and lower bounds for the length of an IGMP message.
Definition: igmp_client.h:59
constexpr u16 TYPE_REPORT_V1
Reply to IGMPv1 query.
Definition: igmp_client.h:68
constexpr u16 MASK_MAXLEN
Bit-mask "length" from first header word.
Definition: igmp_client.h:66
constexpr u16 RECORD_MODE_EXCL
Record type: Exclude mode.
Definition: igmp_client.h:73
u32 decode_maxdly(u16 hdr, bool v3)
Given the first word of the IGMP query header, decode max delay.
Definition: igmp_client.cc:60
constexpr u16 TYPE_REPORT_V2
Reply to IGMPv2 query.
Definition: igmp_client.h:69
bool igmp_send(satcat5::ip::Dispatch *iface, const satcat5::ip::Addr &dst, const unsigned wcount, u16 *data)
Given IGMP message contents, write Eth/IP headers and send packet.
Definition: igmp_client.cc:26
constexpr u16 TYPE_QUERY
IGMP query (all versions)
Definition: igmp_client.h:67
constexpr u16 MASK_TYPE
Bit-mask "type" from first header word.
Definition: igmp_client.h:65
Client state for an IGMP multicast subscription.
Definition: igmp_client.h:145
IPv4 address is a 32-bit unsigned integer.
Definition: ip_core.h:15
u32 value
Raw access to the underlying representation.
Definition: ip_core.h:17
bool is_multicast() const
IP multicast (224.*.*.*)
Definition: ip_core.cc:41
bool is_broadcast() const
Limited broadcast (255.255.255.255)
Definition: ip_core.cc:37
Structure for holding an IPv4 Header, including options.
Definition: ip_core.h:180
void write_to(satcat5::io::Writeable *wr) const
Write IPv4 header to the designated stream.
Definition: ip_core.cc:82
void option_alert()
}
Definition: ip_core.cc:106
Multipurpose filter for matching fields in network packets.
Definition: net_type.h:38
Miscellaneous mathematical utility functions.
constexpr unsigned min_unsigned(unsigned a, unsigned b)
Min and max functions.
Definition: utils.h:111
bool countdown(T &timer, const T &decr)
Decrement a countdown timer, returning true if it reaches zero.
Definition: utils.h:85
satcat5::util::Prng prng
Global instance of the Prng class.
Definition: utils.cc:20