This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new fc5ce4a1d7a2 CAMEL-25245: camel-snmp - GET_NEXT walk must stop at the
end of the MIB view and fail on a timeout (#27242)
fc5ce4a1d7a2 is described below
commit fc5ce4a1d7a27201e688fb4d18870fe93a1860ea
Author: allthingssecurity <[email protected]>
AuthorDate: Fri Oct 2 15:07:48 2026 +0530
CAMEL-25245: camel-snmp - GET_NEXT walk must stop at the end of the MIB
view and fail on a timeout (#27242)
The SNMP GET_NEXT walk now ends at endOfMibView (SNMPv2c/v3) and fails the
exchange on a timeout instead of returning a silently partial result. An agent
answering with an OID inside the subtree that does not increase now fails the
exchange with a CamelExchangeException ("OID not increasing", like net-snmp's
snmpwalk) instead of repeating the request forever. Documented in the upgrade
guide.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../apache/camel/component/snmp/SnmpProducer.java | 36 +++-
.../camel/component/snmp/WalkOIDEndTest.java | 215 +++++++++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 10 +
3 files changed, 252 insertions(+), 9 deletions(-)
diff --git
a/components/camel-snmp/src/main/java/org/apache/camel/component/snmp/SnmpProducer.java
b/components/camel-snmp/src/main/java/org/apache/camel/component/snmp/SnmpProducer.java
index e48532bbddbf..57cc2553dd18 100644
---
a/components/camel-snmp/src/main/java/org/apache/camel/component/snmp/SnmpProducer.java
+++
b/components/camel-snmp/src/main/java/org/apache/camel/component/snmp/SnmpProducer.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
+import org.apache.camel.CamelExchangeException;
import org.apache.camel.Exchange;
import org.apache.camel.support.DefaultProducer;
import org.slf4j.Logger;
@@ -143,24 +144,41 @@ public class SnmpProducer extends DefaultProducer {
while (matched) {
ResponseEvent responseEvent = snmp.send(this.pdu,
this.target);
if (responseEvent == null ||
responseEvent.getResponse() == null) {
- break;
+ throw new TimeoutException("SNMP Producer
Timeout");
}
PDU response = responseEvent.getResponse();
- String nextOid = null;
- List<? extends VariableBinding> variableBindings =
response.getVariableBindings();
- for (int i = 0; i < variableBindings.size(); i++) {
- VariableBinding variableBinding =
variableBindings.get(i);
- nextOid =
variableBinding.getOid().toDottedString();
- if (!nextOid.startsWith(oid.toDottedString())) {
+ if (response.getErrorStatus() == PDU.noSuchName) {
+ // SNMPv1 signals the end of the MIB view with
noSuchName
+ break;
+ }
+ if (response.getErrorStatus() != PDU.noError) {
+ throw new CamelExchangeException(
+ "SNMP walk of " + oid + " failed: " +
response.getErrorStatusText(), exchange);
+ }
+ OID requestedOid = this.pdu.get(0).getOid();
+ VariableBinding next = null;
+ for (VariableBinding variableBinding :
response.getVariableBindings()) {
+ // compare the OIDs, not their strings:
1.3.6.1.4.1.20 is not in the subtree of 1.3.6.1.4.1.2
+ if (!variableBinding.getOid().startsWith(oid)) {
matched = false;
break;
}
+ next = variableBinding;
}
- if (!matched) {
+ // endOfMibView (SNMPv2c/v3) ends the walk
+ if (!matched || next == null || next.isException()) {
break;
}
+ if (next.getOid().compareTo(requestedOid) <= 0) {
+ // a walk on an OID that does not increase would
not end: fail like net-snmp's snmpwalk,
+ // so that a misbehaving agent does not give a
silently partial result
+ throw new CamelExchangeException(
+ "SNMP walk of " + oid + " failed: OID not
increasing: " + requestedOid + " >= "
+ + next.getOid(),
+ exchange);
+ }
this.pdu.clear();
- pdu.add(new VariableBinding(new OID(nextOid)));
+ pdu.add(new VariableBinding(next.getOid()));
smLst.add(new
SnmpMessage(getEndpoint().getCamelContext(), response));
}
}
diff --git
a/components/camel-snmp/src/test/java/org/apache/camel/component/snmp/WalkOIDEndTest.java
b/components/camel-snmp/src/test/java/org/apache/camel/component/snmp/WalkOIDEndTest.java
new file mode 100644
index 000000000000..8e14b1122bdd
--- /dev/null
+++
b/components/camel-snmp/src/test/java/org/apache/camel/component/snmp/WalkOIDEndTest.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.snmp;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelExchangeException;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.test.AvailablePortFinder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.snmp4j.CommandResponder;
+import org.snmp4j.CommandResponderEvent;
+import org.snmp4j.MessageException;
+import org.snmp4j.PDU;
+import org.snmp4j.Snmp;
+import org.snmp4j.mp.StatusInformation;
+import org.snmp4j.smi.Null;
+import org.snmp4j.smi.OID;
+import org.snmp4j.smi.OctetString;
+import org.snmp4j.smi.UdpAddress;
+import org.snmp4j.smi.VariableBinding;
+import org.snmp4j.transport.DefaultUdpTransportMapping;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A GET_NEXT walk must stop at the end of the MIB view and at the end of the
subtree, and fail when the agent does not
+ * answer or answers with an error.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class WalkOIDEndTest extends SnmpTestSupport {
+
+ // nothing listens on this port
+ @RegisterExtension
+ static AvailablePortFinder.Port unusedPort = AvailablePortFinder.find();
+
+ // the GETNEXT answers of the agent: requested OID -> next OID and value
(endOfMibView repeats the requested OID)
+ private static final Map<String, VariableBinding> NEXT = Map.of(
+ "1.3.6.1.4.1.9", new VariableBinding(new OID("1.3.6.1.4.1.9.1.0"),
new OctetString("cisco")),
+ "1.3.6.1.4.1.9.1.0", new VariableBinding(new
OID("1.3.6.1.4.1.9.1.0"), Null.endOfMibView),
+ "1.3.6.1.4.1.2", new VariableBinding(new OID("1.3.6.1.4.1.2.1.0"),
new OctetString("ibm")),
+ "1.3.6.1.4.1.2.1.0", new VariableBinding(new
OID("1.3.6.1.4.1.2021.1.0"), new OctetString("ucd")),
+ "1.3.6.1.4.1.2021.1.0", new VariableBinding(new
OID("1.3.6.1.4.1.3.1.0"), new OctetString("other")),
+ "1.3.6.1.4.1.11", new VariableBinding(new
OID("1.3.6.1.4.1.11.1.0"), new OctetString("hp")),
+ "1.3.6.1.4.1.13", new VariableBinding(new
OID("1.3.6.1.4.1.13.2.0"), new OctetString("first")),
+ // an OID inside the subtree that does not increase
+ "1.3.6.1.4.1.13.2.0", new VariableBinding(new
OID("1.3.6.1.4.1.13.1.0"), new OctetString("back")));
+
+ // the GETNEXT requests the agent answers with an error status and the
requested variable binding:
+ // noSuchName is how an SNMPv1 agent signals the end of the MIB view
+ private static final Map<String, Integer> ERRORS = Map.of(
+ "1.3.6.1.4.1.11.1.0", PDU.noSuchName,
+ "1.3.6.1.4.1.12", PDU.genErr);
+
+ // bounds the requests for one OID, so that a walk that does not stop ends
anyway
+ private static final int MAX_REQUESTS_PER_OID = 20;
+
+ private final Map<String, AtomicInteger> requests = new
ConcurrentHashMap<>();
+ private Snmp agent;
+ private String agentAddress;
+
+ @BeforeAll
+ public void startAgent() throws Exception {
+ DefaultUdpTransportMapping transport = new
DefaultUdpTransportMapping(new UdpAddress("127.0.0.1/0"));
+ agent = new Snmp(transport);
+ agent.addCommandResponder(new CommandResponder() {
+ @Override
+ public void processPdu(CommandResponderEvent event) {
+ PDU request = event.getPDU();
+ if (request.getType() != PDU.GETNEXT) {
+ return;
+ }
+ String oid = request.get(0).getOid().toDottedString();
+ boolean bounded
+ = requests.computeIfAbsent(oid, k -> new
AtomicInteger()).incrementAndGet() > MAX_REQUESTS_PER_OID;
+ VariableBinding next = NEXT.get(oid);
+ Integer errorStatus = ERRORS.get(oid);
+ PDU response = (PDU) request.clone();
+ response.setType(PDU.RESPONSE);
+ // clear() also resets the request id, which the response must
carry
+ response.clear();
+ response.setRequestID(request.getRequestID());
+ if (errorStatus != null && !bounded) {
+ response.add(request.get(0));
+ response.setErrorStatus(errorStatus);
+ response.setErrorIndex(1);
+ } else {
+ if (next == null || bounded) {
+ next = new VariableBinding(new OID("1.3.6.1.6.1.0"),
new OctetString("beyond"));
+ }
+ response.add(next);
+ }
+ try {
+
agent.getMessageDispatcher().returnResponsePdu(event.getMessageProcessingModel(),
+ event.getSecurityModel(), event.getSecurityName(),
event.getSecurityLevel(), response,
+ event.getMaxSizeResponsePDU(),
event.getStateReference(), new StatusInformation());
+ } catch (MessageException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+ });
+ agent.listen();
+ agentAddress =
transport.getListenAddress().toString().replaceFirst("/", ":");
+ }
+
+ @AfterAll
+ public void stopAgent() throws Exception {
+ if (agent != null) {
+ agent.close();
+ }
+ }
+
+ @Test
+ public void testWalkStopsAtEndOfMibView() {
+ List<?> result = walk("direct:endOfMib");
+
+ assertEquals(1, result.size(), "the walk must stop at endOfMibView,
got " + result);
+ }
+
+ @Test
+ public void testWalkStopsAtEndOfSubtree() {
+ List<?> result = walk("direct:subtree");
+
+ assertEquals(1, result.size(), "1.3.6.1.4.1.2021 is not in the subtree
of 1.3.6.1.4.1.2, got " + result);
+ }
+
+ @Test
+ public void testSnmpV1WalkStopsAtNoSuchName() {
+ List<?> result = walk("direct:v1EndOfMib");
+
+ assertEquals(1, result.size(), "the walk must stop at noSuchName, got
" + result);
+ }
+
+ @Test
+ public void testWalkWithAgentErrorFails() {
+ Exchange out = template.request("direct:agentError", e ->
e.getIn().setBody(""));
+
+ assertInstanceOf(CamelExchangeException.class, out.getException(),
"got " + out.getMessage().getBody());
+ }
+
+ @Test
+ public void testWalkWithOidNotIncreasingFails() {
+ Exchange out = template.request("direct:notIncreasing", e ->
e.getIn().setBody(""));
+
+ CamelExchangeException e =
assertInstanceOf(CamelExchangeException.class, out.getException(),
+ "got " + out.getMessage().getBody());
+ assertTrue(e.getMessage().contains("OID not increasing:
1.3.6.1.4.1.13.2.0 >= 1.3.6.1.4.1.13.1.0"), e.getMessage());
+ }
+
+ @Test
+ public void testWalkWithoutAnswerFails() {
+ Exchange out = template.request("direct:noAgent", e ->
e.getIn().setBody(""));
+
+ assertInstanceOf(TimeoutException.class, out.getException(), "got " +
out.getMessage().getBody());
+ }
+
+ private List<?> walk(String uri) {
+ Exchange out = template.request(uri, e -> e.getIn().setBody(""));
+ assertNull(out.getException());
+ return out.getMessage().getBody(List.class);
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ public void configure() {
+ from("direct:endOfMib")
+
.toF("snmp:%s?protocol=udp&snmpVersion=1&type=GET_NEXT&oids=1.3.6.1.4.1.9",
agentAddress);
+
+ from("direct:subtree")
+
.toF("snmp:%s?protocol=udp&snmpVersion=1&type=GET_NEXT&oids=1.3.6.1.4.1.2",
agentAddress);
+
+ from("direct:v1EndOfMib")
+
.toF("snmp:%s?protocol=udp&snmpVersion=0&type=GET_NEXT&oids=1.3.6.1.4.1.11",
agentAddress);
+
+ from("direct:agentError")
+
.toF("snmp:%s?protocol=udp&snmpVersion=1&type=GET_NEXT&oids=1.3.6.1.4.1.12",
agentAddress);
+
+ from("direct:notIncreasing")
+
.toF("snmp:%s?protocol=udp&snmpVersion=1&type=GET_NEXT&oids=1.3.6.1.4.1.13",
agentAddress);
+
+ from("direct:noAgent")
+
.toF("snmp:127.0.0.1:%d?protocol=udp&snmpVersion=1&type=GET_NEXT&oids=1.3.6.1.4.1.9"
+ + "&timeout=200&retries=0",
+ unusedPort.getPort());
+ }
+ };
+ }
+}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 7f612b5bcf92..4d595f816a14 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -746,6 +746,16 @@ written in the charset that the `Content-Type` declares.
Bodies that are not a `
read such a message as UTF-8 must now use the declared charset, and characters
that the declared charset cannot
represent are written as `?`.
+=== camel-snmp - GET_NEXT (walk)
+
+A `GET_NEXT` walk now ends at the end of the agent's MIB view (`endOfMibView`,
or `noSuchName` for SNMPv1) and at
+the end of the requested subtree by comparing OIDs (`1.3.6.1.4.1.20...` is no
longer taken as part of
+`1.3.6.1.4.1.2`). Before, a walk that reached the end of the MIB view
requested the same OID again forever. A walk
+whose request times out now fails with a `TimeoutException`, like `GET`,
instead of returning the entries read so far
+(an empty list for an agent that does not answer), and an agent error other
than `noSuchName` fails the exchange.
+An agent that answers with an OID inside the subtree that does not increase
now also fails the exchange with a
+`CamelExchangeException` ("OID not increasing", like net-snmp's `snmpwalk`),
instead of repeating the request.
+
=== Components and Language removal
==== camel-csimple, camel-csimple-joor and csimple-maven-plugin