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