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

Reply via email to