You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/02 09:37:55 UTC

(camel) branch main updated: CAMEL-25245: camel-snmp - GET_NEXT walk must stop at the end of the MIB view and fail on a timeout (#27242)

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 <49...@users.noreply.github.com>
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 <no...@anthropic.com>
---
 .../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