SatCat5
igmp_server.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/eth_sw_log.h>
7 #include <satcat5/igmp_client.h>
8 #include <satcat5/igmp_server.h>
9 #include <satcat5/ip_dispatch.h>
10 #include <satcat5/utils.h>
11 
14 using satcat5::ip::Addr;
15 using satcat5::ip::ADDR_NONE;
17 
18 // Set the polling interval for response timers.
19 static constexpr unsigned TIMER_INTERVAL_MSEC = 10;
20 
21 // Internal status flags.
22 constexpr u8 FLAG_QUERIER = (1u << 0);
23 
24 void Group::reset(const Addr& addr) {
25  group = addr;
26  mask_active = 0;
27  mask_rcvd1 = 0;
28  mask_rcvd2 = 0;
29  query_count = 0;
30  query_timer = 0;
31 }
32 
33 void Group::rcvd(SATCAT5_PMASK_TYPE mask) {
34  satcat5::util::set_mask(mask_active, mask);
35  satcat5::util::set_mask(mask_rcvd1, mask);
36  satcat5::util::set_mask(mask_rcvd2, mask);
37 }
38 
39 void Group::refresh() {
40  // End of reporting interval, update membership mask.
41  // Merging reports over a few consecutive query/response
42  // intervals reduces impact of a single lost packet.
43  mask_active = mask_rcvd1 | mask_rcvd2;
44  mask_rcvd1 = mask_rcvd2;
45  mask_rcvd2 = 0;
46  // If there are no members, delete this group.
47  if (!mask_active) reset(ADDR_NONE);
48 }
49 
52  satcat5::ip::Dispatch* iface,
53  Group* groups, unsigned gcount)
54  : PluginCore(sw)
55  , m_iface(iface) // May be null
56  , m_groups(groups)
57  , m_gcount(gcount)
58  , m_status(iface ? FLAG_QUERIER : 0)
59  , m_mdly_fast(20)
60  , m_mdly_slow(100)
61  , m_vmin(3)
62  , m_vmax(3)
63  , m_query_timer(iface ? 1000 : 0)
64  , m_window_timer(0)
65 {
66  timer_every(TIMER_INTERVAL_MSEC);
67 }
68 
69 #if SATCAT5_ALLOW_DELETION
70 Server::~Server() {
71  // No additional cleanup required.
72 }
73 #endif
74 
75 void Server::flush() {
76  for (u16 a = 0 ; a < m_gcount ; ++a)
77  m_groups[a].reset(ADDR_NONE);
78  if (m_iface) send_query(nullptr, true);
79 }
80 
82  // Special cases for reserved addresses.
83  if (addr == DST_ALL_SYSTEMS) return satcat5::eth::PMASK_ALL;
84  if (addr == DST_ALL_ROUTERS) return satcat5::eth::PMASK_ALL;
85 
86  // Otherwise, search across active groups.
87  auto group = find_match(addr);
88  return group ? group->mask_active : 0;
89 }
90 
91 bool Server::is_querier() const {
92  return m_status & FLAG_QUERIER;
93 }
94 
96  if (pkt.is_ip() && pkt.ip.proto() == satcat5::ip::PROTO_IGMP) {
97  rcvd_igmp(pkt); // Process incoming IGMP packet.
98  } else if (pkt.is_ip() && pkt.ip.dst().is_multicast()) {
99  rcvd_mcast(pkt); // Route incoming multicast packet.
100  }
101 }
102 
103 u8 Server::version() const {
104  return satcat5::util::min_u8(m_vmin, m_vmax);
105 }
106 
108  // Check global timer for the active query/response window.
109  // (This may be driven by us, or by another network device.)
110  if (countdown<u32>(m_window_timer, TIMER_INTERVAL_MSEC)) {
111  for (u16 a = 0 ; a < m_gcount ; ++a)
112  m_groups[a].refresh();
113  }
114 
115  // Separately, check the timer for sending another query.
116  // If we are the querier, this is concurrent with end-of-window.
117  // If not, this represents a timeout for re-election of a new querier.
118  if (countdown<u32>(m_query_timer, TIMER_INTERVAL_MSEC)) {
119  if (m_iface) send_query(nullptr, true);
120  }
121 
122  // Check rapid-refresh timers for specific groups.
123  // Rapid request/response up to N times, then stop.
124  for (u16 a = 0 ; a < m_gcount ; ++a) {
125  if (countdown<u16>(m_groups[a].query_timer, TIMER_INTERVAL_MSEC)) {
126  m_groups[a].refresh();
127  if (m_iface && ++m_groups[a].query_count < 3) {
128  send_query(m_groups + a, false);
129  }
130  }
131  }
132 }
133 
134 Group* Server::find_or_create(const Addr& group) {
135  bool valid = group.is_multicast() && !group.is_broadcast();
136  if (!valid) return nullptr; // Valid multicast address?
137  Group* match = find_match(group);
138  if (match) return match; // Found a match
139  Group* empty = find_match(ADDR_NONE);
140  if (empty) empty->reset(group);
141  return empty; // Create a new entry
142 }
143 
144 // Brute-force search over the entire array...
145 Group* Server::find_match(const Addr& group) const {
146  Group* match = nullptr;
147  for (u16 a = 0 ; a < m_gcount && !match ; ++a) {
148  if (m_groups[a].group == group) match = m_groups + a;
149  }
150  return match;
151 }
152 
153 void Server::rcvd_igmp(satcat5::eth::PluginPacket& pkt) {
154  // For routers, this method is called for incoming packets from other
155  // devices AND for our own outgoing packets. Forward or drop accordingly.
156  if (m_iface) { // Router mode?
157  // Forward self-generated outgoing IGMP packets, but do not process.
158  if (pkt.ip.src() == m_iface->ipaddr()) return;
159  // Process incoming IGMP packets, but do not forward.
160  pkt.dst_mask = 0;
162  }
163 
164  // Sanity check incoming packet length.
165  unsigned len = pkt.ip.len_inner();
166  if (len > MAX_LEN_BYTES || len < MIN_LEN_BYTES) return;
167  if (pkt.ip.frg()) return; // Fragmentation not supported
168 
169  // Skip headers and get ready to read message contents.
170  // (Note: Ethernet frame may be zero-padded beyond IP end-of-frame.)
172  raw.read_consume(pkt.hlen);
173  satcat5::io::LimitedRead msg(&raw, len);
174 
175  // Validate the IGMP checksum.
176  u16 rcvd[MAX_LEN_SHORTS];
177  unsigned wcount = 0;
178  while (msg.get_read_ready())
179  rcvd[wcount++] = msg.read_u16();
180  u16 chk = satcat5::ip::checksum(wcount, rcvd);
181  if (chk) return; // Expect total = zero.
182 
183  // Decode header fields.
184  u16 type = rcvd[0] & MASK_TYPE;
185  u16 mdly = rcvd[0] & MASK_MAXLEN;
186  Addr addr(rcvd[2], rcvd[3]);
187 
188  // Update minimum version for this subnet.
189  if (type == TYPE_REPORT_V1 && m_vmin > 1) m_vmin = 1;
190  if (type == TYPE_REPORT_V2 && m_vmin > 2) m_vmin = 2;
191 
192  // Sort by type header...
193  if (type == TYPE_QUERY) {
194  // Watch for queries from other routers. The router with the lowest
195  // IP address is elected as the official "querier". Everybody else
196  // should be operating in silent mode, snooping on those queries.
197  // TODO: Handling of S-flag, QRV, QQIC, etc...
198  if (pkt.ip.src().value < m_iface->ipaddr().value) {
199  // Clear the active-querier status flag.
200  satcat5::util::clr_mask_u8(m_status, FLAG_QUERIER);
201  // Set a watchdog timeout, in case the current querier halts.
202  u32 max_dly = decode_maxdly(mdly, wcount > 8);
203  if (m_iface) m_query_timer = 2*max_dly;
204  m_window_timer = max_dly;
205  }
206  } else if (type == TYPE_REPORT_V1 || type == TYPE_REPORT_V2) {
207  // Membership report for a single address/group.
208  auto group = find_or_create(addr);
209  if (group) group->rcvd(pkt.src_mask());
210  } else if (type == TYPE_REPORT_V3) {
211  // Membership report with multiple records...
212  unsigned rcount = rcvd[3];
213  unsigned rdpos = 4;
214  for (unsigned a = 0 ; a < rcount && rdpos + 4 <= wcount ; ++a) {
215  // Read each record, ignoring source address filters.
216  u16 aux = rcvd[rdpos+0] & MASK_MAXLEN;
217  u16 rec = rcvd[rdpos+1];
218  Addr dst(rcvd[rdpos+2], rcvd[rdpos+3]);
219  rdpos += 4 + 2*aux + 2*rec;
220  // Update group membership flags.
221  auto group = find_or_create(dst);
222  if (group) group->rcvd(pkt.src_mask());
223  }
224  } else if (type == TYPE_LEAVE_V2 && m_iface) {
225  // If any member leaves, immediately refresh that group.
226  // (Other endpoints on the same port may still be members.)
227  auto group = find_match(addr);
228  if (group) send_query(group, true);
229  }
230 }
231 
232 void Server::rcvd_mcast(satcat5::eth::PluginPacket& pkt) {
233  // Leave broadcast packets in broadcast mode.
234  Addr dst = pkt.ip.dst();
235  if (dst.is_broadcast()) return;
236 
237  // Check destination mask based on multicast address.
238  pkt.dst_mask &= get_mask(dst);
239 }
240 
241 void Server::send_query(Group* group, bool first) {
242  // Set MDLY parameter for this query.
243  u16 mdly = group ? m_mdly_fast : m_mdly_slow;
244  if (version() == 1) mdly = 0;
245 
246  // Set the status flag indicating we are the elected querier.
247  satcat5::util::set_mask_u8(m_status, FLAG_QUERIER);
248 
249  // Window length adds ~25% to catch stragglers.
250  u32 msec = (decode_maxdly(mdly, version() == 3) * 5) / 4;
251 
252  // Sending this message opens a window for responses.
253  // Set a timer to take action at the end of that interval.
254  const Addr addr = group ? group->group : ADDR_NONE;
255  if (group) {
256  // Rapid queries for a specific multicast address.
257  if (first) group->query_count = 0;
258  group->query_timer = msec;
259  } else {
260  // Slower pace for general queries.
261  m_query_timer = msec;
262  m_window_timer = msec;
263  }
264 
265  // Write the message header.
266  u16 msg[MAX_LEN_SHORTS];
267  unsigned wcount = 0;
268  msg[wcount++] = TYPE_QUERY | mdly;
269  msg[wcount++] = 0; // Placeholder for checksum
270  msg[wcount++] = u16(addr.value >> 16);
271  msg[wcount++] = u16(addr.value >> 0);
272  if (version() == 3) { // Additional fields for IGMPv3?
273  msg[wcount++] = mdly; // TODO: Handle QQIC correctly.
274  msg[wcount++] = 0; // No source filters
275  }
276  igmp_send(m_iface, DST_ALL_SYSTEMS, wcount, msg);
277 }
A shared-memory Ethernet switch based on the MultiBuffer class.
Definition: eth_switch.h:93
Router/Server for the Internet Group Management Protocol (IGMP).
Definition: igmp_server.h:74
bool is_querier() const
Are we the current querier?
Definition: igmp_server.cc:91
Server(satcat5::eth::SwitchCore *sw, satcat5::ip::Dispatch *iface, satcat5::igmp::Group *groups, unsigned gcount)
Link this plugin to the network switch or router.
Definition: igmp_server.cc:50
void query(satcat5::eth::PluginPacket &pkt) override
Packet-processing callback from PluginCore.
Definition: igmp_server.cc:95
u8 version() const
Current IGMP version for this network.
Definition: igmp_server.cc:103
void flush()
Flush all existing routes and reset status.
Definition: igmp_server.cc:75
SATCAT5_PMASK_TYPE get_mask(const satcat5::ip::Addr &addr) const
Get the port-mask for a given multicast IP address.
Definition: igmp_server.cc:81
void timer_event() override
Child class MUST override this method.
Definition: igmp_server.cc:107
Limited read of next N bytes.
Definition: io_readable.h:255
Barebones class for reading data from a MultiPacket.
Definition: multi_buffer.h:129
Protocol handler and dispatch unit for Internet Protocol v4 (IPv4).
Definition: ip_dispatch.h:43
void timer_every(unsigned msec)
Configure a repeating notification every X milliseconds.
Definition: polling.cc:321
Diagnostic logging system for the Ethernet switch.
#define SATCAT5_PMASK_TYPE
Set the integer type used to identify source and destination ports.
Definition: eth_switch.h:41
Client-side implementation of the Internet Group Management Protocol (IGMP)
constexpr unsigned MAX_LEN_SHORTS
Set upper and lower bounds for the length of an IGMP message.
Definition: igmp_client.h:58
constexpr satcat5::ip::Addr DST_ALL_ROUTERS(224, 0, 0, 2)
< All endpoints on subnet.
constexpr u16 MASK_MAXLEN
Bit-mask "length" from first header word.
Definition: igmp_client.h:66
u32 decode_maxdly(u16 hdr, bool v3)
Given the first word of the IGMP query header, decode max delay.
Definition: igmp_client.cc:60
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
Router-side implementation of the Internet Group Management Protocol (IGMP)
Ephemeral data structure provided to plugin callbacks.
Definition: eth_plugin.h:48
satcat5::io::MultiPacket * pkt
Complete packet contents.
Definition: eth_plugin.h:51
satcat5::ip::Header ip
Copy of additional header fields, if present.
Definition: eth_plugin.h:62
bool is_ip() const
Accessors and shortcuts for packet metadata.
Definition: eth_plugin.h:129
u16 hlen
Original header length.
Definition: eth_plugin.h:78
u16 flags
Additional status flags indicating packet status.
Definition: eth_plugin.h:81
SATCAT5_PMASK_TYPE dst_mask
Destination mask for which port(s) receive this packet.
Definition: eth_plugin.h:75
SATCAT5_PMASK_TYPE src_mask() const
Accessors and shortcuts for packet metadata.
Definition: eth_plugin.h:139
static constexpr u8 DROP_MCTRL
Link-local control packet.
Definition: eth_sw_log.h:82
Router state for monitoring IGMP multicast subscriptions.
Definition: igmp_server.h:41
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
constexpr unsigned len_inner() const
< Inner bytes excluding header
Definition: ip_core.h:200
constexpr u8 proto() const
< Inner protocol (UDP/TCP/etc.)
Definition: ip_core.h:204
constexpr u16 frg() const
< Fragment offset
Definition: ip_core.h:198
constexpr satcat5::ip::Addr dst() const
< Destination address
Definition: ip_core.h:210
constexpr satcat5::ip::Addr src() const
< Source address
Definition: ip_core.h:208
Miscellaneous mathematical utility functions.
void clr_mask_u8(volatile u8 &val, u8 mask)
Set or clear bit masks.
Definition: utils.h:22
void set_mask_u8(volatile u8 &val, u8 mask)
Set or clear bit masks.
Definition: utils.h:21
bool countdown(T &timer, const T &decr)
Decrement a countdown timer, returning true if it reaches zero.
Definition: utils.h:85
void set_mask(T &val, T mask)
Set or clear bit masks.
Definition: utils.h:32