Skip to content

hostcnnConnect() re-throws IOException preventing fallback to as-database when QIBM_HOST_CNN is inactive #321

Description

@fbortoli

Description

In JTOpen v21.0.6, JDBC connections to IBM i (AS/400) systems fail in SSL/TLS contexts when the QIBM_HOST_CNN (Host Connection Server) service is not active on the target system.

The driver attempts to connect via the as-hostcnn service first, and is expected to fall back to the traditional as-database path if as-hostcnn is unavailable. However, due to an overly broad exception handler in hostcnnConnect(), certain IOException subclasses (notably SSLHandshakeException, SSLException, SocketException, SocketTimeoutException) are re-thrown instead of being caught silently — preventing the fallback to the traditional connection path.

JTOpen v11.2.2, which does not contain any hostcnn code, connects correctly using the direct as-database path.

Steps to Reproduce

  1. Target IBM i system with QIBM_HOST_CNN service not active
  2. Use JTOpen v21.0.6 JDBC driver
  3. Attempt a JDBC connection over SSL/TLS:
    Connection conn = DriverManager.getConnection(
        "jdbc:as400://HOSTNAME;translate binary=true;prompt=false",
        "USER", "PASSWORD");
  4. Connection fails with an SSLHandshakeException or similar IOException instead of falling back to the traditional as-database path

Expected Behavior

When as-hostcnn is unavailable (service not active), hostcnnConnect() should fail silently (like it does for ConnectException), leaving hostcnnServer_ as null, so that getConnection() proceeds to the fallback path via PortMapper.getServerSocket("as-database") at line 1501.

Actual Behavior

The IOException from the failed SSL handshake to as-hostcnn is re-thrown, propagating up to getConnection() and aborting the entire connection attempt. The fallback code at line 1495 (if (hostcnnServer_ == null)) is never reached.

Root Cause Analysis

File: com/ibm/as400/access/AS400ImplRemote.java
Method: hostcnnConnect() (line ~3718)
Lines: ~3891-3895

The second catch block in hostcnnConnect() combines IOException, AS400SecurityException, and RuntimeException into a single handler that re-throws all of them:

catch (ConnectException | ServerStartupException e)
{
    if (e instanceof ServerStartupException
        && ((ServerStartupException) e).getReturnCode()
            != ServerStartupException.CONNECTION_PORT_CANNOT_CONNECT_TO)
        throw e;
    // Caught silently — hostcnnServer_ stays null — fallback works ✓
    Trace.log(Trace.DIAGNOSTIC, "The server as-hostcnn is not up...");
}
catch (IOException | AS400SecurityException | RuntimeException e) {
    // BUG: ALL IOExceptions are re-thrown, including SSL/socket exceptions
    Trace.log(Trace.ERROR, "Hostcnn server exchange client/server attributes failed:", e);
    throw e;  // ← prevents fallback for SSL/IO exceptions
}

The first catch correctly handles ConnectException (TCP connection refused) and ServerStartupException silently. However, the second catch also captures SSLException, SSLHandshakeException, SocketException, and SocketTimeoutException — all of which are subclasses of IOException — and re-throws them.

Java exception hierarchy involved:

java.io.IOException
├── java.net.SocketException
│   └── java.net.ConnectException          ← caught by 1st catch (OK)
├── javax.net.ssl.SSLException             ← caught by 2nd catch (BUG: re-thrown)
│   ├── SSLHandshakeException              ← caught by 2nd catch (BUG: re-thrown)
│   └── SSLProtocolException               ← caught by 2nd catch (BUG: re-thrown)
├── java.net.SocketTimeoutException        ← caught by 2nd catch (BUG: re-thrown)
└── ServerStartupException                 ← caught by 1st catch (OK)

Since as-hostcnn is always SSL (see PortMapper.java lines 182-185 where as-hostcnn is unconditionally suffixed with -s), when the service is not active, the failure manifests as an SSL exception rather than a plain ConnectException.

Connection flow showing the failure

AS400JDBCDriver.connect()
└─> AS400.connectService(AS400.DATABASE)
    └─> AS400ImplRemote.getConnection(service=DATABASE)
        ├─> hostcnnConnect(true)
        │   └─> PortMapper.getServerSocket("as-hostcnn")
        │       └─> SSL handshake FAILS → SSLHandshakeException
        │           └─> catch (IOException | ...) → throw e  ← BUG
        │               └─> Exception propagates up
        │                   └─> getConnection() FAILS
        │
        └─> if (hostcnnServer_ == null)  ← NEVER REACHED
            └─> PortMapper.getServerSocket("as-database")  ← FALLBACK NEVER USED

Proposed Fix

Split the combined catch block into three separate catch blocks, treating IOException as a non-fatal connectivity error (same as ConnectException), while continuing to re-throw AS400SecurityException and RuntimeException:

catch (ConnectException | ServerStartupException e)
{
    if (e instanceof ServerStartupException
        && ((ServerStartupException) e).getReturnCode()
            != ServerStartupException.CONNECTION_PORT_CANNOT_CONNECT_TO)
        throw e;
    Trace.log(Trace.DIAGNOSTIC,
        "The server as-hostcnn is not up and thus cannot be used for authentication");
}
catch (IOException e) {
    // When the QIBM_HOST_CNN service is not active, the connection attempt to as-hostcnn
    // can fail with various IOExceptions (SocketException, SSLException, connection reset,
    // etc.), especially in SSL contexts. Instead of re-throwing and failing the entire
    // connection, treat this the same as ConnectException and allow fallback to the
    // traditional as-database connection path.
    Trace.log(Trace.WARNING,
        "The server as-hostcnn connection failed with IOException, " +
        "falling back to direct connection: " + e.getMessage());
    if (Trace.traceOn_)
        Trace.log(Trace.DIAGNOSTIC, "Hostcnn IOException details:", e);
}
catch (AS400SecurityException e) {
    Trace.log(Trace.ERROR, "Hostcnn server authentication failed:", e);
    throw e;
}
catch (RuntimeException e) {
    Trace.log(Trace.ERROR, "Hostcnn server unexpected error:", e);
    throw e;
}

Why this is safe:

Scenario Behavior
QIBM_HOST_CNN active + SSL working hostcnnConnect() succeeds — catch block never reached, no impact
QIBM_HOST_CNN not active + SSL IOException caught silently → hostcnnServer_ remains null → fallback to as-database works
QIBM_HOST_CNN not active + no SSL ConnectException caught by first catch (existing behavior, unchanged)
Invalid credentials AS400SecurityException re-thrown (unchanged)
Programming error RuntimeException re-thrown (unchanged)

The catch order is correct because ConnectException and ServerStartupException (both extend IOException) are matched by the first catch before the general IOException catch.

Versions

  • Affected: v21.0.6 (and likely all versions since hostcnn support was introduced)
  • Not affected: v11.2.2 and earlier (no hostcnn code)

Environment

  • IBM i (AS/400) target system with QIBM_HOST_CNN service not active
  • SSL/TLS enabled JDBC connections
  • Java 8+ runtime

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions