DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
snmp_agent.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file snmp_agent.cpp
6 * @brief SNMP v1/v2c agent: MIB table, PDU dispatch, and the UDP binding.
7 */
8
10
11#if DETWS_ENABLE_SNMP
12
13#include <string.h>
14
15#if DETWS_ENABLE_SNMP_V3
17#endif
18
19#if defined(ARDUINO)
20#include <Arduino.h>
21static uint32_t snmp_uptime_cs()
22{
23 return (uint32_t)(millis() / 10ULL); // hundredths of a second since boot
24}
25#else
26static uint32_t snmp_uptime_cs()
27{
28 return 0; // host build has no clock; tests assert type, not value
29}
30#endif
31
32// ---------------------------------------------------------------------------
33// MIB table + community configuration (all in BSS - no heap)
34// ---------------------------------------------------------------------------
35
36struct SnmpMibEntry
37{
38 uint32_t oid[SNMP_MAX_OID_LEN];
39 size_t oid_len;
40 SnmpValue val; // static value (used when getter == nullptr)
41 SnmpGetFn getter; // dynamic value provider (optional)
42 SnmpSetFn setter; // writable hook (optional)
43};
44
45// All persistent SNMP-agent config, owned by one instance (internal linkage): the MIB table
46// and its count plus the read-only / read-write community strings, grouped so it is one named
47// owner, unreachable from any other translation unit. The MIB lookups take it by const
48// reference so processing an attacker's PDU provably cannot mutate the MIB.
49struct SnmpAgentCtx
50{
51 SnmpMibEntry mib[SNMP_MAX_MIB_ENTRIES];
52 size_t mib_count = 0;
54 char rw[SNMP_COMMUNITY_MAX] = "";
55 bool rw_set = false;
56};
57static SnmpAgentCtx s_agent;
58
59// sysObjectID value (private enterprise placeholder: 1.3.6.1.4.1.49374).
60static const uint32_t g_sys_object_id[] = {1, 3, 6, 1, 4, 1, 49374};
61
62// ---------------------------------------------------------------------------
63// OID helpers
64// ---------------------------------------------------------------------------
65
66static int oid_cmp(const uint32_t *a, size_t an, const uint32_t *b, size_t bn)
67{
68 size_t n = an < bn ? an : bn;
69 for (size_t i = 0; i < n; i++)
70 {
71 if (a[i] < b[i])
72 return -1;
73 if (a[i] > b[i])
74 return 1;
75 }
76 if (an < bn)
77 return -1;
78 if (an > bn)
79 return 1;
80 return 0;
81}
82
83static const SnmpMibEntry *mib_find_exact(const SnmpAgentCtx &c, const uint32_t *oid, size_t n)
84{
85 for (size_t i = 0; i < c.mib_count; i++)
86 if (oid_cmp(c.mib[i].oid, c.mib[i].oid_len, oid, n) == 0)
87 return &c.mib[i];
88 return nullptr;
89}
90
91// Smallest registered OID that is strictly greater than (oid,n), or nullptr.
92static const SnmpMibEntry *mib_find_next(const SnmpAgentCtx &c, const uint32_t *oid, size_t n)
93{
94 const SnmpMibEntry *best = nullptr;
95 for (size_t i = 0; i < c.mib_count; i++)
96 {
97 if (oid_cmp(c.mib[i].oid, c.mib[i].oid_len, oid, n) > 0)
98 {
99 if (!best || oid_cmp(c.mib[i].oid, c.mib[i].oid_len, best->oid, best->oid_len) < 0)
100 best = &c.mib[i];
101 }
102 }
103 return best;
104}
105
106static bool fetch_value(const SnmpMibEntry *en, SnmpValue *out)
107{
108 if (en->getter)
109 return en->getter(out);
110 *out = en->val;
111 return true;
112}
113
114// RFC 3416 4.2.1: a GetRequest for an unbound name reports noSuchInstance when the
115// name's OBJECT IDENTIFIER prefix matches a known object (only the instance is
116// absent), and noSuchObject when no such object exists at all. Every registered
117// entry is a full instance OID, so its object-type prefix is its OID minus the
118// final (instance) arc; the request names a known object iff that prefix is a
119// prefix of the request.
120static bool mib_object_exists(const SnmpAgentCtx &c, const uint32_t *oid, size_t n)
121{
122 for (size_t i = 0; i < c.mib_count; i++)
123 {
124 size_t objn = c.mib[i].oid_len - 1; // drop the trailing instance arc
125 if (objn <= n && oid_cmp(c.mib[i].oid, objn, oid, objn) == 0)
126 return true;
127 }
128 return false;
129}
130
131// ---------------------------------------------------------------------------
132// Registration
133// ---------------------------------------------------------------------------
134
135static SnmpMibEntry *mib_alloc(SnmpAgentCtx &c, const uint32_t *oid, size_t n)
136{
137 if (c.mib_count >= SNMP_MAX_MIB_ENTRIES || n < 2 || n > SNMP_MAX_OID_LEN)
138 return nullptr;
139 SnmpMibEntry *e = &c.mib[c.mib_count++];
140 memset(e, 0, sizeof(*e));
141 for (size_t i = 0; i < n; i++)
142 e->oid[i] = oid[i];
143 e->oid_len = n;
144 return e;
145}
146
147void snmp_agent_init(const char *ro_community)
148{
149 s_agent.mib_count = 0;
150 s_agent.rw_set = false;
151 s_agent.rw[0] = '\0';
152 const char *ro = (ro_community && ro_community[0]) ? ro_community : DETWS_SNMP_DEFAULT_RO_COMMUNITY;
153 strncpy(s_agent.ro, ro, sizeof(s_agent.ro) - 1);
154 s_agent.ro[sizeof(s_agent.ro) - 1] = '\0';
155}
156
157void snmp_agent_set_rw_community(const char *rw_community)
158{
159 if (!rw_community || !rw_community[0])
160 {
161 s_agent.rw_set = false;
162 s_agent.rw[0] = '\0';
163 return;
164 }
165 strncpy(s_agent.rw, rw_community, sizeof(s_agent.rw) - 1);
166 s_agent.rw[sizeof(s_agent.rw) - 1] = '\0';
167 s_agent.rw_set = true;
168}
169
170bool snmp_agent_add_string(const uint32_t *oid, size_t oid_len, const char *value, SnmpSetFn setter)
171{
172 SnmpMibEntry *e = mib_alloc(s_agent, oid, oid_len);
173 if (!e)
174 return false;
175 e->val.type = (uint8_t)SnmpTag::BER_OCTET_STRING;
176 e->val.str = value;
177 e->val.str_len = value ? strnlen(value, SNMP_MSG_BUF_SIZE) : 0;
178 e->setter = setter;
179 return true;
180}
181
182bool snmp_agent_add_integer(const uint32_t *oid, size_t oid_len, long value, SnmpSetFn setter)
183{
184 SnmpMibEntry *e = mib_alloc(s_agent, oid, oid_len);
185 if (!e)
186 return false;
187 e->val.type = (uint8_t)SnmpTag::BER_INTEGER;
188 e->val.ival = value;
189 e->setter = setter;
190 return true;
191}
192
193bool snmp_agent_add_dynamic(const uint32_t *oid, size_t oid_len, uint8_t type, SnmpGetFn getter)
194{
195 SnmpMibEntry *e = mib_alloc(s_agent, oid, oid_len);
196 if (!e)
197 return false;
198 e->val.type = type;
199 e->getter = getter;
200 return true;
201}
202
203static bool sys_uptime_get(SnmpValue *out)
204{
205 out->type = (uint8_t)SnmpTag::SNMP_TIMETICKS;
206 out->uval = snmp_uptime_cs();
207 return true;
208}
209
210void snmp_agent_set_system(const char *descr, const char *contact, const char *name, const char *location,
211 long services)
212{
213 const uint32_t o_descr[] = {1, 3, 6, 1, 2, 1, 1, 1, 0};
214 const uint32_t o_oid[] = {1, 3, 6, 1, 2, 1, 1, 2, 0};
215 const uint32_t o_uptime[] = {1, 3, 6, 1, 2, 1, 1, 3, 0};
216 const uint32_t o_contact[] = {1, 3, 6, 1, 2, 1, 1, 4, 0};
217 const uint32_t o_name[] = {1, 3, 6, 1, 2, 1, 1, 5, 0};
218 const uint32_t o_loc[] = {1, 3, 6, 1, 2, 1, 1, 6, 0};
219 const uint32_t o_svc[] = {1, 3, 6, 1, 2, 1, 1, 7, 0};
220
221 snmp_agent_add_string(o_descr, 9, descr);
222
223 SnmpMibEntry *e = mib_alloc(s_agent, o_oid, 9);
224 if (e)
225 {
226 e->val.type = (uint8_t)SnmpTag::BER_OID;
227 e->val.oid = g_sys_object_id;
228 e->val.oid_len = sizeof(g_sys_object_id) / sizeof(g_sys_object_id[0]);
229 }
230
231 snmp_agent_add_dynamic(o_uptime, 9, (uint8_t)SnmpTag::SNMP_TIMETICKS, sys_uptime_get);
232 snmp_agent_add_string(o_contact, 9, contact);
233 snmp_agent_add_string(o_name, 9, name);
234 snmp_agent_add_string(o_loc, 9, location);
235 snmp_agent_add_integer(o_svc, 9, services);
236}
237
238// ---------------------------------------------------------------------------
239// Value encode / decode
240// ---------------------------------------------------------------------------
241
242static void enc_value(BerEnc *e, const SnmpValue *v)
243{
244 switch (v->type)
245 {
246 case (uint8_t)SnmpTag::BER_INTEGER:
247 ber_put_integer(e, v->ival);
248 break;
249 case (uint8_t)SnmpTag::BER_OCTET_STRING:
250 case (uint8_t)SnmpTag::SNMP_OPAQUE:
251 ber_put_octet_string(e, v->type, (const uint8_t *)v->str, v->str_len);
252 break;
253 case (uint8_t)SnmpTag::BER_OID:
254 ber_put_oid(e, v->oid, v->oid_len);
255 break;
256 case (uint8_t)SnmpTag::SNMP_TIMETICKS:
257 case (uint8_t)SnmpTag::SNMP_COUNTER32:
258 case (uint8_t)SnmpTag::SNMP_GAUGE32:
259 ber_put_uint(e, v->type, v->uval);
260 break;
261 case (uint8_t)SnmpTag::SNMP_IPADDRESS: {
262 uint8_t ip[4] = {(uint8_t)(v->uval >> 24), (uint8_t)(v->uval >> 16), (uint8_t)(v->uval >> 8),
263 (uint8_t)(v->uval)};
264 ber_put_octet_string(e, (uint8_t)SnmpTag::SNMP_IPADDRESS, ip, 4);
265 break;
266 }
267 case (uint8_t)SnmpTag::BER_NULL:
268 ber_put_null(e);
269 break;
270 default:
271 // Exception markers (noSuchObject / noSuchInstance / endOfMibView): the
272 // tag with a zero-length, no-value encoding.
273 ber_put_tlv(e, v->type, nullptr, 0);
274 break;
275 }
276}
277
278// Read one varbind value TLV at the decoder cursor into @p v (OID values decode
279// into @p oidbuf). Advances the cursor past the value.
280static bool dec_value(BerDec *d, SnmpValue *v, uint32_t *oidbuf)
281{
282 memset(v, 0, sizeof(*v));
283 size_t save = d->pos;
284 uint8_t tag;
285 size_t len;
286 if (!ber_read_header(d, &tag, &len))
287 return false;
288 v->type = tag;
289 switch (tag)
290 {
291 case (uint8_t)SnmpTag::BER_INTEGER:
292 d->pos = save;
293 return ber_read_integer(d, &v->ival);
294 case (uint8_t)SnmpTag::SNMP_TIMETICKS:
295 case (uint8_t)SnmpTag::SNMP_COUNTER32:
296 case (uint8_t)SnmpTag::SNMP_GAUGE32:
297 case (uint8_t)SnmpTag::SNMP_IPADDRESS: {
298 uint32_t acc = 0;
299 for (size_t i = 0; i < len; i++)
300 acc = (acc << 8) | d->buf[d->pos + i];
301 v->uval = acc;
302 d->pos += len;
303 return true;
304 }
305 case (uint8_t)SnmpTag::BER_OCTET_STRING:
306 case (uint8_t)SnmpTag::SNMP_OPAQUE:
307 v->str = (const char *)(d->buf + d->pos);
308 v->str_len = len;
309 d->pos += len;
310 return true;
311 case (uint8_t)SnmpTag::BER_OID:
312 d->pos = save;
313 if (!ber_read_oid(d, oidbuf, SNMP_MAX_OID_LEN, &v->oid_len))
314 return false;
315 v->oid = oidbuf;
316 return true;
317 default: // NULL (GET requests) and anything else: skip the value bytes
318 d->pos += len;
319 return true;
320 }
321}
322
323// ---------------------------------------------------------------------------
324// Per-request scratch (single-threaded: lwIP callback or test, never reentrant)
325// ---------------------------------------------------------------------------
326
327struct InVb
328{
329 uint32_t oid[SNMP_MAX_OID_LEN];
330 size_t oid_len;
331 SnmpValue val;
332 uint32_t valoid[SNMP_MAX_OID_LEN]; // backing store if the value is an OID
333};
334
335struct OutVb
336{
337 const uint32_t *oid;
338 size_t oid_len;
339 SnmpValue val;
340};
341
342// All per-request decode/encode scratch, owned by one instance (internal linkage): the input
343// and output varbind arrays. Single-threaded (the lwIP callback or a test, never reentrant),
344// so it is one named owner, unreachable from any other translation unit.
345struct SnmpReqCtx
346{
347 InVb in[SNMP_MAX_VARBINDS];
348 OutVb out[SNMP_MAX_VARBINDS];
349};
350static SnmpReqCtx s_req;
351
352// Run the SET varbind loop: apply each varbind, stopping at the first error. On failure writes the
353// SnmpErr and the 1-based varbind index into the err_status and err_index outputs (left unchanged if
354// all succeed; v2c selects the v2c error variant). Extracted so the per-varbind guards are not nested
355// 4 deep (S134).
356static void snmp_apply_set_all(size_t nvb, bool v2c, long *err_status, long *err_index)
357{
358 for (size_t i = 0; i < nvb; i++)
359 {
360 const SnmpMibEntry *en = mib_find_exact(s_agent, s_req.in[i].oid, s_req.in[i].oid_len);
361 int e = 0;
362 if (!en)
363 e = (int)SnmpErr::SNMP_ERR_NO_SUCH_NAME;
364 else if (!en->setter)
365 e = (!v2c) ? (int)SnmpErr::SNMP_ERR_READ_ONLY : (int)SnmpErr::SNMP_ERR_NOT_WRITABLE;
366 else if (!en->setter(&s_req.in[i].val))
367 e = (!v2c) ? (int)SnmpErr::SNMP_ERR_BAD_VALUE : (int)SnmpErr::SNMP_ERR_WRONG_TYPE;
368 if (!e)
369 continue;
370 *err_status = e;
371 *err_index = (long)(i + 1);
372 return;
373 }
374}
375
376static bool community_eq(const char *stored, const char *p, size_t len)
377{
378 return stored[0] != '\0' && strnlen(stored, len + 1) == len && memcmp(stored, p, len) == 0;
379}
380
381static size_t encode_pdu(long request_id, long err_status, long err_index, const OutVb *out, size_t nout, uint8_t *buf,
382 size_t cap)
383{
384 BerEnc e;
385 ber_enc_init(&e, buf, cap);
386 size_t pdu = ber_seq_begin(&e, (uint8_t)SnmpTag::SNMP_PDU_RESPONSE);
387 ber_put_integer(&e, request_id);
388 ber_put_integer(&e, err_status);
389 ber_put_integer(&e, err_index);
390 size_t vbl = ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
391 for (size_t i = 0; i < nout; i++)
392 {
393 size_t vb = ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
394 ber_put_oid(&e, out[i].oid, out[i].oid_len);
395 enc_value(&e, &out[i].val);
396 ber_seq_end(&e, vb);
397 }
398 ber_seq_end(&e, vbl);
399 ber_seq_end(&e, pdu);
400 return e.ok ? e.len : 0;
401}
402
403// Process one request PDU (a Get/GetNext/GetBulk/Set TLV) against the MIB and
404// emit one GetResponse PDU. Shared by the v1/v2c community framing and the v3
405// USM layer. @p v2c selects v2c-style per-varbind exceptions over v1
406// error-status; @p allow_write authorizes Set.
407size_t snmp_dispatch_pdu(const uint8_t *pdu, size_t pdu_len, bool allow_write, bool v2c, uint8_t *out, size_t out_cap)
408{
409 BerDec d;
410 ber_dec_init(&d, pdu, pdu_len);
411
412 uint8_t pdu_tag;
413 size_t pdu_clen;
414 if (!ber_read_header(&d, &pdu_tag, &pdu_clen))
415 return 0;
416
417 long request_id;
418 long field2;
419 long field3;
420 if (!ber_read_integer(&d, &request_id) || !ber_read_integer(&d, &field2) || !ber_read_integer(&d, &field3))
421 return 0;
422
423 uint8_t vbl_tag;
424 size_t vbl_len;
425 if (!ber_read_header(&d, &vbl_tag, &vbl_len) || vbl_tag != (uint8_t)SnmpTag::BER_SEQUENCE)
426 return 0;
427 size_t vbl_end = d.pos + vbl_len;
428
429 // Decode the request varbind list.
430 size_t nvb = 0;
431 while (d.pos < vbl_end && d.ok)
432 {
433 if (nvb >= SNMP_MAX_VARBINDS)
434 return 0;
435 uint8_t vt;
436 size_t vlen;
437 if (!ber_read_header(&d, &vt, &vlen) || vt != (uint8_t)SnmpTag::BER_SEQUENCE)
438 return 0;
439 if (!ber_read_oid(&d, s_req.in[nvb].oid, SNMP_MAX_OID_LEN, &s_req.in[nvb].oid_len))
440 return 0;
441 if (!dec_value(&d, &s_req.in[nvb].val, s_req.in[nvb].valoid))
442 return 0;
443 nvb++;
444 }
445 if (!d.ok) // GCOVR_EXCL_LINE every failing read in the loop already returns 0 via its own guard,
446 return 0; // GCOVR_EXCL_LINE so d.ok is always true here (defense in depth)
447
448 long err_status = (int)SnmpErr::SNMP_ERR_NO_ERROR;
449 long err_index = 0;
450 size_t nout = 0;
451
452 if (pdu_tag == (uint8_t)SnmpTag::SNMP_PDU_GET || pdu_tag == (uint8_t)SnmpTag::SNMP_PDU_GETNEXT)
453 {
454 for (size_t i = 0; i < nvb; i++)
455 {
456 const SnmpMibEntry *en = (pdu_tag == (uint8_t)SnmpTag::SNMP_PDU_GET)
457 ? mib_find_exact(s_agent, s_req.in[i].oid, s_req.in[i].oid_len)
458 : mib_find_next(s_agent, s_req.in[i].oid, s_req.in[i].oid_len);
459 SnmpValue val;
460 bool ok = en && fetch_value(en, &val);
461 if (!ok)
462 {
463 if (!v2c)
464 {
465 // Classic error reporting: echo the request varbinds.
466 err_status = (int)SnmpErr::SNMP_ERR_NO_SUCH_NAME;
467 err_index = (long)(i + 1);
468 nout = nvb;
469 for (size_t k = 0; k < nvb; k++)
470 {
471 s_req.out[k].oid = s_req.in[k].oid;
472 s_req.out[k].oid_len = s_req.in[k].oid_len;
473 s_req.out[k].val = s_req.in[k].val;
474 }
475 break;
476 }
477 // v2c: per-varbind exception. For Get, distinguish a missing
478 // instance of a known object (en found but its getter declined, or
479 // the object's type-prefix is registered) from an unknown object;
480 // GetNext past the end of the MIB is always endOfMibView.
481 if (pdu_tag == (uint8_t)SnmpTag::SNMP_PDU_GET)
482 {
483 bool inst = en || mib_object_exists(s_agent, s_req.in[i].oid, s_req.in[i].oid_len);
484 val.type = inst ? (uint8_t)SnmpTag::SNMP_NO_SUCH_INSTANCE : (uint8_t)SnmpTag::SNMP_NO_SUCH_OBJECT;
485 }
486 else
487 val.type = (uint8_t)SnmpTag::SNMP_END_OF_MIB_VIEW;
488 s_req.out[nout].oid = en ? en->oid : s_req.in[i].oid;
489 s_req.out[nout].oid_len = en ? en->oid_len : s_req.in[i].oid_len;
490 s_req.out[nout].val = val;
491 nout++;
492 continue;
493 }
494 s_req.out[nout].oid = (pdu_tag == (uint8_t)SnmpTag::SNMP_PDU_GETNEXT) ? en->oid : s_req.in[i].oid;
495 s_req.out[nout].oid_len =
496 (pdu_tag == (uint8_t)SnmpTag::SNMP_PDU_GETNEXT) ? en->oid_len : s_req.in[i].oid_len;
497 s_req.out[nout].val = val;
498 nout++;
499 }
500 }
501 else if (pdu_tag == (uint8_t)SnmpTag::SNMP_PDU_GETBULK)
502 {
503 if (!v2c) // GetBulk is v2c+
504 return 0;
505 long non_rep = field2 < 0 ? 0 : field2;
506 long max_rep = field3 < 0 ? 0 : field3;
507 if ((size_t)non_rep > nvb)
508 non_rep = (long)nvb;
509
510 // Non-repeaters: one GetNext each.
511 for (long i = 0; i < non_rep && nout < SNMP_MAX_VARBINDS; i++)
512 {
513 const SnmpMibEntry *en = mib_find_next(s_agent, s_req.in[i].oid, s_req.in[i].oid_len);
514 if (en)
515 {
516 s_req.out[nout].oid = en->oid;
517 s_req.out[nout].oid_len = en->oid_len;
518 fetch_value(en, &s_req.out[nout].val);
519 }
520 else
521 {
522 s_req.out[nout].oid = s_req.in[i].oid;
523 s_req.out[nout].oid_len = s_req.in[i].oid_len;
524 s_req.out[nout].val.type = (uint8_t)SnmpTag::SNMP_END_OF_MIB_VIEW;
525 }
526 nout++;
527 }
528
529 // Repeaters: walk each remaining varbind up to max_rep successors.
530 size_t nrep = nvb - (size_t)non_rep;
531 const uint32_t *cur_oid[SNMP_MAX_VARBINDS];
532 size_t cur_len[SNMP_MAX_VARBINDS];
533 bool ended[SNMP_MAX_VARBINDS];
534 for (size_t r = 0; r < nrep; r++)
535 {
536 cur_oid[r] = s_req.in[non_rep + r].oid;
537 cur_len[r] = s_req.in[non_rep + r].oid_len;
538 ended[r] = false;
539 }
540 for (long rep = 0; rep < max_rep && nout < SNMP_MAX_VARBINDS; rep++)
541 {
542 for (size_t r = 0; r < nrep && nout < SNMP_MAX_VARBINDS; r++)
543 {
544 if (ended[r])
545 {
546 s_req.out[nout].oid = cur_oid[r];
547 s_req.out[nout].oid_len = cur_len[r];
548 s_req.out[nout].val.type = (uint8_t)SnmpTag::SNMP_END_OF_MIB_VIEW;
549 nout++;
550 continue;
551 }
552 const SnmpMibEntry *en = mib_find_next(s_agent, cur_oid[r], cur_len[r]);
553 if (en)
554 {
555 s_req.out[nout].oid = en->oid;
556 s_req.out[nout].oid_len = en->oid_len;
557 fetch_value(en, &s_req.out[nout].val);
558 cur_oid[r] = en->oid;
559 cur_len[r] = en->oid_len;
560 }
561 else
562 {
563 s_req.out[nout].oid = cur_oid[r];
564 s_req.out[nout].oid_len = cur_len[r];
565 s_req.out[nout].val.type = (uint8_t)SnmpTag::SNMP_END_OF_MIB_VIEW;
566 ended[r] = true;
567 }
568 nout++;
569 }
570 }
571 }
572 else if (pdu_tag == (uint8_t)SnmpTag::SNMP_PDU_SET)
573 {
574 // SET uses error-status/error-index in both v1 and v2c; echo the varbinds.
575 nout = nvb;
576 for (size_t k = 0; k < nvb; k++)
577 {
578 s_req.out[k].oid = s_req.in[k].oid;
579 s_req.out[k].oid_len = s_req.in[k].oid_len;
580 s_req.out[k].val = s_req.in[k].val;
581 }
582 if (!allow_write)
583 {
584 err_status = (!v2c) ? (int)SnmpErr::SNMP_ERR_NO_SUCH_NAME : (int)SnmpErr::SNMP_ERR_NO_ACCESS;
585 err_index = 1;
586 }
587 else
588 {
589 snmp_apply_set_all(nvb, v2c, &err_status, &err_index);
590 }
591 }
592 else
593 {
594 return 0; // unsupported PDU (Trap, Response, etc.)
595 }
596
597 size_t n = encode_pdu(request_id, err_status, err_index, s_req.out, nout, out, out_cap);
598 if (n == 0 && nout > 0)
599 {
600 // Response didn't fit: report tooBig with an empty varbind list.
601 n = encode_pdu(request_id, (int)SnmpErr::SNMP_ERR_TOO_BIG, 0, s_req.out, 0, out, out_cap);
602 }
603 return n;
604}
605
606// ---------------------------------------------------------------------------
607// Message wrapper: v1/v2c community framing (v3 delegates to the USM layer)
608// ---------------------------------------------------------------------------
609
610size_t snmp_agent_process(const uint8_t *req, size_t req_len, uint8_t *resp, size_t resp_cap)
611{
612 BerDec d;
613 ber_dec_init(&d, req, req_len);
614
615 uint8_t tag;
616 size_t len;
617 if (!ber_read_header(&d, &tag, &len) || tag != (uint8_t)SnmpTag::BER_SEQUENCE) // message wrapper
618 return 0;
619
620 long version;
621 if (!ber_read_integer(&d, &version))
622 return 0;
623
624 if (version == (int)SnmpVersion::SNMP_V3)
625 {
626#if DETWS_ENABLE_SNMP_V3
627 return snmp_v3_process(req, req_len, resp, resp_cap);
628#else
629 return 0; // v3 needs the gated USM layer (DETWS_ENABLE_SNMP_V3)
630#endif
631 }
632 if (version != (int)SnmpVersion::SNMP_V1 && version != (int)SnmpVersion::SNMP_V2C)
633 return 0;
634
635 uint8_t ctag;
636 size_t clen;
637 if (!ber_read_header(&d, &ctag, &clen) || ctag != (uint8_t)SnmpTag::BER_OCTET_STRING)
638 return 0;
639 const char *community = (const char *)(d.buf + d.pos);
640 size_t community_len = clen;
641 d.pos += clen;
642
643 bool is_rw = s_agent.rw_set && community_eq(s_agent.rw, community, community_len);
644 bool is_ro = is_rw || community_eq(s_agent.ro, community, community_len);
645 if (!is_ro) // unknown community: silently drop, like a standard agent
646 return 0;
647
648 // The PDU is the remaining bytes of the datagram; dispatch and re-wrap.
649 static uint8_t pdubuf[SNMP_MSG_BUF_SIZE];
650 size_t pn = snmp_dispatch_pdu(req + d.pos, req_len - d.pos, is_rw, version == (int)SnmpVersion::SNMP_V2C, pdubuf,
651 sizeof(pdubuf));
652 if (pn == 0)
653 return 0;
654
655 BerEnc e;
656 ber_enc_init(&e, resp, resp_cap);
657 size_t msg = ber_seq_begin(&e, (uint8_t)SnmpTag::BER_SEQUENCE);
658 ber_put_integer(&e, version);
659 ber_put_octet_string(&e, (uint8_t)SnmpTag::BER_OCTET_STRING, (const uint8_t *)community, community_len);
660 ber_put_raw(&e, pdubuf, pn);
661 ber_seq_end(&e, msg);
662 return e.ok ? e.len : 0;
663}
664
665// ---------------------------------------------------------------------------
666// UDP binding (via the transport-layer UDP service - no lwIP here)
667// ---------------------------------------------------------------------------
668
670
671// All SNMP UDP-binding state, owned by one instance (internal linkage): the response scratch
672// (the request is transport-owned), so it is one named owner, unreachable cross-TU.
673namespace
674{
675struct SnmpUdpCtx
676{
677 uint8_t tx[SNMP_MSG_BUF_SIZE];
678};
679SnmpUdpCtx s_snmp_udp;
680} // namespace
681
682static void snmp_udp_handler(const uint8_t *data, size_t len, struct DetUdpPeer *peer, void *ctx)
683{
684 (void)ctx;
685 size_t rn = snmp_agent_process(data, len, s_snmp_udp.tx, sizeof(s_snmp_udp.tx));
686 if (rn)
687 det_udp_send(peer, s_snmp_udp.tx, rn);
688}
689
690void snmp_agent_begin_udp(uint16_t port)
691{
692 det_udp_listen(port, snmp_udp_handler, nullptr);
693}
694
695#endif // DETWS_ENABLE_SNMP
#define SNMP_MAX_OID_LEN
Maximum sub-identifiers (arcs) in an SNMP object identifier.
#define DETWS_SNMP_DEFAULT_RO_COMMUNITY
Default read-only community (overridable at runtime via snmp_agent_init). Deployments SHOULD change t...
#define SNMP_MSG_BUF_SIZE
Static request/response datagram buffers for the SNMP UDP agent.
#define SNMP_COMMUNITY_MAX
Maximum SNMP community-string length (including null terminator).
#define SNMP_MAX_MIB_ENTRIES
Maximum registered MIB objects (the agent's fixed OID table).
#define SNMP_MAX_VARBINDS
Maximum variable bindings the agent will emit in one response.
Zero-heap SNMP v1/v2c agent: PDU processing + a fixed MIB table.
SNMPv3 User-based Security Model (USM) layer (DETWS_ENABLE_SNMP_V3).
bool det_udp_send(struct DetUdpPeer *peer, const uint8_t *data, size_t len)
Send a datagram back to the peer captured during the handler call.
Definition udp.cpp:178
bool det_udp_listen(uint16_t port, DetUdpHandler handler, void *ctx)
Bind a UDP port and route incoming datagrams to handler.
Definition udp.cpp:151
Layer 4 (Transport) - connectionless UDP datagram service.