Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ target
*.iml
.idea/
.gradle
.gradle-user-home/
gradle/*
!gradle/wrapper/
!gradle/wrapper/gradle-wrapper.jar
Expand All @@ -27,4 +28,12 @@ log-test/

.DS_Store
.factorypath
settings.json
settings.json

# Generated integration-test container image artifacts
/src/test/resources/*-docker-*.tgz
/src/test/resources/manifest.json
/src/test/resources/repositories
/src/test/resources/[0-9a-f]*.json
/src/test/resources/[0-9a-f]*.tar
/src/test/resources/[0-9a-f]*/
99 changes: 77 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ mvn clean package
```
(The wrapper script downloads the correct Gradle version automatically.)

To run the live NETCONF integration suite with Gradle:

```bash
NETCONF_HOST=192.168.1.1 \
NETCONF_USERNAME=admin \
NETCONF_PASSWORD=secret \
NETCONF_PORT=830 \
./gradlew integrationTest
```

Releases
========
Releases contain source code only. Due to changing JDK licensing, jar files are not released.
Expand All @@ -47,11 +57,28 @@ User may download the source code and compile it with desired JDK version.
* Instructions to build using `mvn`
* Download Source Code for the required release
* Compile the code and build the jar using `mvn package`
* Use the jar file from (source to netconf-java)/netconf-java/target
* Use the jar file from `./target/`
* Use `mvn versions:display-dependency-updates` to identify possible target versions for dependencies

=======

v2.2.1
------
* Hardened NETCONF XML parsing against XXE and DTD-based attacks
* Fixed NETCONF RPC framing and `message-id` reply correlation for sequential session reuse
* Enforced shared NETCONF base capability negotiation and derive session framing from the negotiated base version
* Capability-gated candidate, validate, and confirmed-commit operations before sending RPCs
* Added negotiated capability inspection via `Device.getNegotiatedCapabilities()` and `NetconfSession.getNegotiatedCapabilities()`
* Typed NETCONF `<rpc-error>` replies as structured exceptions so callers can inspect server-reported error details
* Added `ValidateException` and clarified `validate()` semantics: server `rpc-error` replies throw, while warning-only or other non-`<ok/>` non-error replies still return `false`
* Improved SSH/NETCONF session cleanup on failed connection or session initialization
* Fixed shell exec helpers so commands are set, channels are connected, and timeout/cleanup behavior is more predictable
* Fixed nested XML path construction in the XML helper
* Documented `NetconfSession` as a sequential request/response channel rather than a concurrent in-flight RPC transport
* Added [`docs/compatibility.md`](docs/compatibility.md) with current RFC, capability, NMDA, and extension support details
* Added a dedicated Gradle `integrationTest` task that forwards NETCONF connection settings for live-server testing
* Upgraded `assertj-core` to `3.27.7` to address `CVE-2026-24400`

v2.2.0
------
* Java 17 baseline; compiled with `--release 17`
Expand Down Expand Up @@ -96,6 +123,7 @@ SYNOPSIS
import java.io.IOException;
import javax.xml.parsers.ParserConfigurationException;
import net.juniper.netconf.NetconfException;
import net.juniper.netconf.ValidateException;
import org.xml.sax.SAXException;

import net.juniper.netconf.XML;
Expand All @@ -105,31 +133,58 @@ public class ShowInterfaces {
public static void main(String args[]) throws NetconfException,
ParserConfigurationException, SAXException, IOException {

//Create device
Device device = Device.builder()
.hostName("hostname")
.userName("username")
.password("password")
.connectionTimeout(2000)
.hostKeysFileName("hostKeysFileName")
.build();
device.connect();

//Send RPC and receive RPC Reply as XML
XML rpc_reply = device.executeRPC("get-interface-information");
/* OR
* device.executeRPC("<get-interface-information/>");
* OR
* device.executeRPC("<rpc><get-interface-information/></rpc>");
*/

//Print the RPC-Reply and close the device.
System.out.println(rpc_reply);
device.close();
try (Device device = Device.builder()
.hostName("hostname")
.userName("username")
.password("password")
.hostKeysFileName("hostKeysFileName")
.connectionTimeout(2000)
.commandTimeout(5000)
.build()) {

// Establish the SSH transport and the default NETCONF session.
device.connect();

// Send RPC and receive RPC reply as XML.
XML rpcReply = device.executeRPC("get-interface-information");
/* OR
* device.executeRPC("<get-interface-information/>");
* OR
* device.executeRPC("<rpc><get-interface-information/></rpc>");
*/

System.out.println(rpcReply);
}
}
}
```

Candidate validate example:

```Java
try {
boolean clean = device.validate();
if (!clean) {
// Warning-only or other non-error, non-<ok/> reply.
System.out.println("Validate completed without rpc-error, but did not return <ok/>");
}
} catch (ValidateException e) {
// Server returned one or more <rpc-error> elements.
System.err.println("Validate failed: " + e.getMessage());
e.getRpcErrors().forEach(System.err::println);
}
```

Recommended usage:

* Build one `Device` per target connection and use `try-with-resources` so SSH resources are released predictably.
* Call `connect()` before issuing RPCs. If `connect()` throws, no usable NETCONF session was established.
* Inspect `getNegotiatedCapabilities()` after `connect()` if your application needs to branch on server support for candidate, validate, or confirmed-commit behavior.
* Set `connectionTimeout` and `commandTimeout` explicitly for production use rather than relying on defaults.
* Prefer NETCONF RPC helpers (`executeRPC`, `getConfig`, `loadXMLConfiguration`, `commit`, and friends) for device operations; use shell helpers only for device-specific workflows that are not available over NETCONF.
* Treat `ValidateException` as the server-side `rpc-error` path for `validate()`. A `false` return now means the reply was non-error but not a clean `<ok/>`, typically warnings.
* Shell helper reads are bounded by `commandTimeout`. If you use `runShellCommandRunning(...)`, always close the returned reader so the underlying exec channel is released.

LICENSE
=======

Expand Down
73 changes: 70 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ plugins {
}

group = 'net.juniper.netconf'
version = '2.2.0.0'
version = '2.2.1.0'
description = 'An API For NetConf client'

java {
Expand All @@ -26,7 +26,7 @@ dependencies {
implementation 'com.google.guava:guava:32.0.1-jre'

testImplementation 'org.hamcrest:hamcrest-all:1.3'
testImplementation 'org.assertj:assertj-core:3.23.1'
testImplementation 'org.assertj:assertj-core:3.27.7'
testImplementation 'org.mockito:mockito-core:4.8.1'
testImplementation 'commons-io:commons-io:2.14.0'
testImplementation 'org.xmlunit:xmlunit-assertj:2.9.0'
Expand All @@ -50,6 +50,73 @@ test {
}
}

def netconfPropertySpecs = [
[name: 'netconf.host', env: 'NETCONF_HOST', required: true],
[name: 'netconf.username', env: 'NETCONF_USERNAME', required: true],
[name: 'netconf.password', env: 'NETCONF_PASSWORD', required: true],
[name: 'netconf.port', env: 'NETCONF_PORT', required: false, defaultValue: '830'],
[name: 'netconf.timeout', env: 'NETCONF_TIMEOUT', required: false, defaultValue: '30000']
]

def resolveNetconfProperty = { Map spec ->
if (project.hasProperty(spec.name)) {
return project.property(spec.name).toString()
}
if (System.getProperty(spec.name) != null) {
return System.getProperty(spec.name)
}
if (System.getenv(spec.env) != null) {
return System.getenv(spec.env)
}
return spec.defaultValue
}

tasks.register('integrationTest', org.gradle.api.tasks.testing.Test) {
description = 'Runs live NETCONF integration tests against a configured endpoint.'
group = 'verification'
useJUnitPlatform()
shouldRunAfter test
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath

filter {
includeTestsMatching 'net.juniper.netconf.integration.NetconfIntegrationTest'
}

testLogging {
events "passed", "skipped", "failed"
}

doFirst {
def resolvedProperties = [:]
def missingRequiredProperties = []

netconfPropertySpecs.each { spec ->
def value = resolveNetconfProperty(spec)
if (value == null || value.toString().trim().isEmpty()) {
if (spec.required) {
missingRequiredProperties << "${spec.name} (${spec.env})"
}
return
}
resolvedProperties[spec.name] = value.toString()
}

if (!missingRequiredProperties.isEmpty()) {
throw new org.gradle.api.GradleException(
"integrationTest requires live NETCONF connection details. " +
"Provide ${missingRequiredProperties.join(', ')} via -Dnetconf.* " +
"or NETCONF_* environment variables."
)
}

systemProperty 'netconf.integration.enabled', 'true'
resolvedProperties.each { propertyName, propertyValue ->
systemProperty propertyName, propertyValue
}
}
}

tasks.withType(JavaCompile).configureEach {
options.fork = true
options.forkOptions.jvmArgs += [
Expand Down Expand Up @@ -104,4 +171,4 @@ publishing {
}
}
}
}
}
92 changes: 92 additions & 0 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Compatibility Matrix

This document describes what `netconf-java` currently implements. It is an implementation matrix, not a blanket compliance claim. Interoperability still depends on the server's advertised capabilities and on whether the application stays within the library's supported session model.

Mental model: today the library is a synchronous, JSch-backed NETCONF-over-SSH client. Its strongest path is one sequential RPC conversation per `NetconfSession`, with explicit timeouts and explicit cleanup. Core NETCONF 1.0 and 1.1 framing is supported, several Junos workflows are wrapped ergonomically, and optional features are beginning to be enforced through capability negotiation, though coverage is not yet uniform across all extensions.

## Status legend

| Status | Meaning |
| --- | --- |
| `Supported` | Implemented and intended for normal use. |
| `Supported with caveats` | Implemented, but there are negotiation, API-surface, or interoperability caveats. |
| `Partial` | Some pieces exist, but coverage is incomplete or not first-class. |
| `Not implemented` | No dedicated support today. |

## Core NETCONF and transport

| Standard / feature | Status | Notes |
| --- | --- | --- |
| RFC 6242 SSH subsystem transport | `Supported` | `Device` opens an SSH subsystem channel with `subsystem=netconf` over JSch. |
| RFC 6241 `<hello>` parsing and generation | `Supported with caveats` | `Hello` parsing/building works, `Hello.builder()` auto-adds `urn:ietf:params:netconf:base:1.1`, and session establishment now fails if the peers do not share a common NETCONF base capability. The remaining caveat is that the client still cannot intentionally advertise only `base:1.0`. |
| NETCONF 1.0 end-of-message framing (`]]>]]>`) | `Supported` | Legacy framing is still supported for both outbound and inbound messages. |
| NETCONF 1.1 chunked framing | `Supported` | Chunked framing is selected when both peers share `urn:ietf:params:netconf:base:1.1`. |
| NETCONF 1.0 server interoperability | `Supported with caveats` | A 1.0-only server can interoperate because the client still advertises `base:1.0` and can read/write legacy framing. |
| NETCONF 1.0-only client advertisement | `Not implemented` | The client cannot intentionally advertise only `base:1.0`; `Hello.builder()` always injects `base:1.1`. |
| RPC `message-id` generation and reply correlation | `Supported with caveats` | Missing `message-id` attributes are injected, replies are validated, and sequential same-session alignment is covered by tests. One `NetconfSession` is still a sequential conversation, not a safe multiplexed channel for concurrent in-flight RPCs. |
| `<rpc-error>` parsing | `Supported` | `RpcReply` parses `error-type`, `error-tag`, `error-severity`, `error-path`, `error-message`, and common `error-info` fields into structured objects. |
| `<close-session>` | `Supported` | `NetconfSession.close()` sends `<close-session/>` and disconnects the channel. |
| `<kill-session>` | `Supported` | `NetconfSession.killSession(String)` is implemented. |
| Secure XML parsing | `Supported` | DTDs and XXE resolution are disabled for `Device`, `Hello`, and `RpcReply` parsing paths. |

## Standard capabilities and common operations

| Capability / operation | Status | Notes |
| --- | --- | --- |
| `<get>` | `Supported` | `getRunningConfigAndState(...)` issues `<get>`. |
| `<get-config>` | `Supported` | Candidate and running helpers exist. |
| `<edit-config>` to candidate | `Supported with caveats` | `loadXMLConfiguration(...)` and `loadTextConfiguration(...)` target `candidate`, and candidate-dependent operations now fail locally when the server did not advertise candidate support. The API still does not expose `test-option` or `error-option`. |
| `:candidate:1.0` | `Supported with caveats` | Candidate-oriented helpers are a primary workflow and are now runtime-gated against the server `<hello>`, but the default client capability still uses the legacy `urn:ietf:params:netconf:base:1.0#candidate` form rather than the RFC 6241 `urn:ietf:params:netconf:capability:candidate:1.0` URN. |
| `<commit>` | `Supported` | Standard commit is implemented. |
| `:confirmed-commit:1.1` | `Supported with caveats` | `commitConfirm(seconds, persistToken)` and `cancelCommit(persistId)` are runtime-gated. Persist-based flows require modern `confirmed-commit:1.1`, while legacy confirmed-commit remains usable for same-session confirmation flows without `persist`. The default client capability advertisement still uses the legacy `base:1.0#confirmed-commit` form. |
| `:validate:1.0` | `Supported with caveats` | `validate()` is implemented against candidate and now fails locally when validate support is absent, but default advertisement still uses the legacy `base:1.0#validate` URI. |
| `<lock>` / `<unlock>` | `Partial` | Candidate lock/unlock helpers exist. There is no first-class running datastore lock helper. |
| `:writable-running:1.0` | `Partial` | Running config retrieval exists, but there is no `edit-config` helper that targets `running` and the capability is not advertised by default. |
| `:startup:1.0` | `Not implemented` | No first-class startup datastore copy/delete flows are present. |
| `:url:1.0` | `Partial` | The default client capability list advertises the legacy `base:1.0#url?protocol=http,ftp,file` form, but there is no dedicated URL-based copy/load API surface. |
| `:xpath:1.0` | `Partial` | The library can pass caller-supplied filter XML through to `<get>` and `<get-data>`, but there is no typed XPath filter API and no runtime capability check. |
| `:rollback-on-error:1.0` | `Not implemented` | No API surface for `error-option rollback-on-error`. |
| `<discard-changes>` | `Not implemented` | No dedicated helper today. |
| `<copy-config>` | `Not implemented` | No dedicated helper today. |
| `<delete-config>` | `Not implemented` | No dedicated helper today. |
| RFC 6243 `with-defaults` | `Not implemented` | No explicit support or helper surface. |

## NMDA and additional datastore support

| Standard / feature | Status | Notes |
| --- | --- | --- |
| RFC 8526 `<get-data>` / `ietf-netconf-nmda` | `Supported with caveats` | `getData(xpathFilter, datastore)` emits NMDA namespace-qualified requests and can target `running`, `candidate`, `startup`, `intended`, or `operational`. The call is not capability-gated; the application must know the server supports NMDA. |
| RFC 8342 datastore naming | `Partial` | `Datastore` includes `RUNNING`, `CANDIDATE`, `STARTUP`, `INTENDED`, and `OPERATIONAL`, but only `getData(...)` uses that model directly. |

## Notifications and subscriptions

| Standard / feature | Status | Notes |
| --- | --- | --- |
| RFC 5277 event notifications | `Not implemented` | No `create-subscription`, no notification stream reader, and no event callback surface. |
| RFC 5277 `:interleave:1.0` | `Not implemented` | No interleaving of notifications with active RPC traffic. |

## Junos-specific and non-standard helpers

| Feature | Status | Notes |
| --- | --- | --- |
| Junos `<load-configuration>` helpers | `Supported` | XML, text, and set-style configuration loading helpers are present. These are vendor-specific, not portable NETCONF. |
| Junos `<commit-configuration><full/>` | `Supported` | `commitFull()` exists and is Junos-specific. |
| Junos CLI command helper | `Supported with caveats` | `runCliCommand(...)` and `runCliCommandRunning(...)` are implemented, but they are not portable to non-Junos servers. |
| Junos configuration mode helpers | `Supported` | `openConfiguration(...)` and `closeConfiguration()` are implemented. |
| SSH exec shell helpers | `Supported with caveats` | `runShellCommand(...)` and `runShellCommandRunning(...)` now connect and clean up channels correctly, and reads are bounded by `commandTimeout`. They still do not provide a richer per-command cancellation model beyond timeout and channel close. |
| Device reboot helper | `Supported with caveats` | `reboot()` exists as a convenience helper, but it is server-specific rather than a portable standards abstraction. |

## Interoperability caveats worth knowing

- Default optional capability advertisement still uses legacy `urn:ietf:params:netconf:base:1.0#...` forms in `Device.DEFAULT_CLIENT_CAPABILITIES`. Many servers accept these, but strict RFC 6241 capability matching may not.
- Candidate, validate, and confirmed-commit flows are now capability-gated before the RPC is sent. Other optional operations are still not enforced uniformly, especially outside the classic RFC 6241 capability set.
- `NetconfSession` should be treated as a single sequential request/response channel. Use separate sessions for concurrent workflows.
- The SSH transport is still tightly coupled to JSch. That preserves the current JSch-based deployment model, including existing FIPS-oriented environments, but transport abstraction is future work rather than current behavior.

## Primary implementation points

- [`Device`](../src/main/java/net/juniper/netconf/Device.java) - SSH transport setup, client capability advertisement, device-level helpers
- [`NetconfSession`](../src/main/java/net/juniper/netconf/NetconfSession.java) - NETCONF framing, message-id handling, core RPC helpers, session lifecycle
- [`Hello`](../src/main/java/net/juniper/netconf/element/Hello.java) - `<hello>` parsing and generation
- [`RpcReply`](../src/main/java/net/juniper/netconf/element/RpcReply.java) - `<rpc-reply>` and `<rpc-error>` parsing
- [`Datastore`](../src/main/java/net/juniper/netconf/element/Datastore.java) - datastore enum used by NMDA-style `<get-data>`
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>net.juniper.netconf</groupId>
<artifactId>netconf-java</artifactId>
<version>2.2.0.0</version>
<version>2.2.1.0</version>
<packaging>jar</packaging>

<properties>
Expand Down Expand Up @@ -190,7 +190,7 @@
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<version>3.27.7</version>
<scope>test</scope>
</dependency>

Expand Down
Loading
Loading