Decompiled ifxjdbc.jar (4.50.JC10, build 146, 2023-03-07) with CFR 0.152
into build/jdbc-src/. The decompiled tree is gitignored — it's a
clean-room understanding reference, not shipped code.
Findings landed in two artifacts:
JDBC_NOTES.md — the reverse-lookup index:
- JAR identity (SHA256, manifest, line counts)
- Package layout (com.informix.{asf,jdbc,lang} are the load-bearing
packages; org.bson and the JDBC API surface get ignored)
- Class index mapping each wire-protocol concern to the responsible
Java class. Highlights:
- com.informix.asf.Connection (the wire transport / login PDU)
- com.informix.asf.IfxData{Input,Output}Stream (framing primitives)
- com.informix.jdbc.IfxMessageTypes (140+ message-tag constants)
- com.informix.lang.JavaToIfxType / IfxToJavaType (codecs)
- com.informix.jdbc.IfxSqli / IfxSqliConnect (the SQLI state machine)
- Auth landscape: plain-password is inline in the binary login PDU;
PAM is a server-initiated post-login challenge/response; CSM is
removed from this driver (literally throws an error if you try)
PROTOCOL_NOTES.md — the byte-level wire-format reference:
- Endianness: big-endian, network byte order (confirmed from
JavaToIfxInt source)
- Width table: SmallInt 2B, Int 4B, BigInt 8B, plus the legacy 10-byte
LongInt that we skip for MVP
- 16-bit alignment requirement for variable-length payloads — every
string/decimal/datetime is 0-padded if odd-length, missing this
desynchronizes the parser
- Login PDU structure decoded byte-by-byte from encodeAscBinary():
SLheader (6 bytes) + PFheader with markers 100/101/104/106/107/
108/116/127, capability bitfield, env vars, process info, app name
- Disconnection: bare [short SQ_EXIT=56] both directions, no header
- Post-login messages have NO header — protocol is stream-oriented:
[short tag][payload][short tag][payload]...
- Message-type tag table categorized by purpose
- Open questions list and cross-check matrix tracking what's
JDBC-derived vs PCAP-confirmed
DECISION_LOG.md additions:
- ifxjdbc.jar 4.50.JC10 selected as JDBC reference; CFR 0.152 as decompiler
- CSM is officially dead — never plan for it
- Plain-password auth is single-round-trip (no challenge/response)
- Wire-framing primitives locked in for _protocol.py
- Container credentials: user=informix, password=in4mix, on port 9088,
TLS off
Phase 0 exit gate: criteria #1 (login layout), #2 (message-type tags),
#3 (SELECT 1 hypothesis) are derived from JDBC. PCAP capture (task #7)
and cross-reference (task #2) remaining to corroborate.
526 lines
20 KiB
Markdown
526 lines
20 KiB
Markdown
# SQLI Wire Protocol Notes
|
||
|
||
> **Phase 0 spike artifact.** Byte-level reference for the Informix SQLI wire protocol, derived from clean-room study of the decompiled IBM Informix JDBC driver (`ifxjdbc.jar`, `Implementation-Version: 4.50.10-SNAPSHOT` / printable `4.50.JC10`, build 146 from 2023-03-07; SHA256 in `JDBC_NOTES.md`) cross-checked against packet captures of the reference exchange (pending).
|
||
|
||
---
|
||
|
||
## Source attribution conventions
|
||
|
||
Each documented byte sequence cites both sources of evidence:
|
||
|
||
- 🟡 **JDBC**: cross-referenced against `<class>#<method>()` in the decompiled tree (see `JDBC_NOTES.md`)
|
||
- 🔵 **PCAP**: observed in `docs/CAPTURES/<file>.pcap` at offset `<n>`
|
||
- ✅ **CONFIRMED**: corroborated by both 🟡 and 🔵
|
||
- 🟠 **UNVERIFIED**: only one of the two sources
|
||
|
||
**Current state** (2026-05-02): all 🟡 findings are present from JDBC reading. PCAP capture is pending (Phase 0 task #7) and required for ✅. Treat everything below as 🟠 pending PCAP cross-check.
|
||
|
||
---
|
||
|
||
## 1. Wire framing primitives 🟡
|
||
|
||
### Endianness
|
||
|
||
**Big-endian (network byte order) for all multi-byte integers.**
|
||
|
||
Source: `com.informix.lang.JavaToIfxType.JavaToIfxInt(int)` line 51-54:
|
||
```java
|
||
public static byte[] JavaToIfxInt(int i) {
|
||
byte[] b = new byte[]{(byte)(i >> 24), (byte)(i >> 16), (byte)(i >> 8), (byte)i};
|
||
return b;
|
||
}
|
||
```
|
||
Same pattern for `JavaToIfxSmallInt` (2 bytes) and `JavaToIfxLongBigInt` (8 bytes). Java's `DataOutputStream` defaults to big-endian; Informix matches that.
|
||
|
||
### Width table
|
||
|
||
| Wire type | Width (bytes) | Java method | Notes |
|
||
|-----------|---------------|-------------|-------|
|
||
| SmallInt | 2 | `writeSmallInt(short)` / `writeShort` | SMALLINT, message-type tags, length prefixes |
|
||
| Int | 4 | `writeInt(int)` | INTEGER, capability flags, sizes |
|
||
| LongBigInt| 8 | `writeLongBigint(long)` | BIGINT (use this for 64-bit ints) |
|
||
| LongInt | 10 | `writeLongInt(long)` | **Legacy variable-numeric LONG INT — skip MVP**, predates 64-bit ints |
|
||
| Real | 4 | `writeReal(float)` | IEEE 754 single |
|
||
| Double | 8 | `writeDouble(double)` | IEEE 754 double |
|
||
| Date | 4 | `writeDate(Date)` | day-count from Informix epoch (1899-12-31, to confirm) |
|
||
|
||
### Variable-length encoding (string, decimal, datetime, interval, BYTE, TEXT)
|
||
|
||
```
|
||
[short length][bytes payload][optional 0x00 pad if length is odd]
|
||
```
|
||
|
||
**The 16-bit alignment is a hard requirement.** Source: `IfxDataOutputStream.writePadded(byte[])`:
|
||
```java
|
||
public void writePadded(byte[] b) throws IOException {
|
||
this.write(b, 0, b.length);
|
||
if ((b.length & 1) >= 1) {
|
||
this.write(0);
|
||
}
|
||
}
|
||
```
|
||
Mirror in `IfxDataInputStream.readPadded`. Every variable-payload message needs this padding or the next short read will misalign and the entire parser desynchronizes.
|
||
|
||
### String encoding
|
||
|
||
`com.informix.lang.JavaToIfxType.JavaToIfxChar(String)` returns:
|
||
```
|
||
[short length+1][bytes][0x00 nul terminator]
|
||
```
|
||
The `+1` is the trailing nul. So an N-character string takes `2 + N + 1` bytes, then padded to even.
|
||
|
||
Note: there is also a `JavaToIfx4BytesChar` variant for wide-char strings. Probably for GLS multibyte locales. Phase 6+.
|
||
|
||
---
|
||
|
||
## 2. Connection establishment 🟡
|
||
|
||
### TCP setup
|
||
|
||
- Default port: **9088** (native SQLI). Port 9089 is SSL.
|
||
- `Socket.connect(InetSocketAddress(host, port), loginTimeout)`
|
||
- `setTcpNoDelay(true)` — Nagle off
|
||
- `setKeepAlive(socKeepAlive)` — opt-in via `IFX_SOC_KEEPALIVE` property
|
||
- Optional SSL wrap via `SSLSocketFactory`. **Phase 6+; skip for MVP.**
|
||
- Buffered streams: `BufferedInputStream(in, 4096)` / `BufferedOutputStream(out, 4096)` over the raw socket streams. The `IfxDataInputStream`/`IfxDataOutputStream` wrap the buffered streams.
|
||
|
||
### Capability bits (driving login path selection)
|
||
|
||
- `capabilities == 0` → legacy text-mode login (`EncodeAscString`)
|
||
- `capabilities > 0` → modern binary login (`encodeAscBinary`) **— this is what we implement**
|
||
|
||
The capabilities bitfield itself is opaque from the client side; the JDBC driver computes it based on opt-props. For MVP we will hardcode a sensible value matching what JDBC sends in a vanilla connection (will derive from PCAP).
|
||
|
||
### Other constants seen in `Connection.java`
|
||
|
||
| Constant | Value | Notes |
|
||
|----------|-------|-------|
|
||
| `MAX_BUFF_SIZE` | 32768 | upper bound for individual PDUs |
|
||
| `MIN_BUFF_SIZE` | 140 | lower bound |
|
||
| `STREAM_BUF_SIZE` | 4096 | socket buffer size |
|
||
| `PFCONREQ_BUF_SIZE` | 2048 | login request buffer |
|
||
| `SL_HEADER_SIZE` | 6 | the SLheader is 6 bytes |
|
||
| `applType` | `"sqlexec |