oracle-tns
Oracle Database Driver

Zero Instant Client.
Pure Rust. Oracle 12c+.

oracle-tns is a pure Rust implementation of the Oracle TNS wire protocol. No Oracle Instant Client, no C bindings, no shared libraries. A single static-musl binary that speaks TNS natively — O3LOGON, O5LOGON, queries, PL/SQL, stored procedures, REF CURSORs.

This driver connects to Oracle 12c R1 with 10G/11G password verifiers — something python-oracledb, node-oracledb, oracle-nio, and kalliope all fail to do (DPY-3015).
~5.5 MB
static musl binary
13ms
avg latency over VPN
5
fuzz targets
48M+
fuzz executions

Quickstart

Connect to Oracle
in three steps.

1. Add dependency
cargo add oracle-tns
2. Configure connection
use oracle_tns::{ConnectConfig, OracleSession};

let config = ConnectConfig {
    host: "db.example.com".to_string(),
    port: 1521,
    service_name: "MYDB".to_string(),
    username: "myuser".to_string(),
    password: "mypassword".to_string(),
};
3. Query
let mut session = OracleSession::connect(&config).await?;

let rows = session.query(
    "SELECT id, name, email FROM users WHERE department = :1",
    &[BindParam::varchar("Engineering")],
).await?;

for row in rows {
    let id: i64 = row.get("ID")?;
    let name: String = row.get("NAME")?;
    println!("{id}: {name}");
}

session.close().await?;

Features

Full Oracle protocol
in pure Rust.

ConnectTNS handshake, O3LOGON (0x939) + O5LOGON auth, service name resolution
QuerySELECT with full row decoding — VARCHAR2, NUMBER, DATE, RAW, NULL
Bind parametersPositional bind variables (:1, :2) — VARCHAR, INTEGER types
PaginationOFFSET/FETCH NEXT support for Oracle 12c+ row limiting
PL/SQL blocksExecute anonymous PL/SQL blocks with bind variables
Stored proceduresCALL with IN and OUT parameters
OUT parametersRead output values from stored procedure calls
REF CURSORBind OUT cursor, fetch result set with column metadata
DMLINSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
Connection poolAsync pool with configurable min/max connections and idle timeout
PingLightweight connection health check over TNS
OxGate proxyTNS proxy with authenticated backend pool and OCI 12.1 byte-perfect mimic — see section below

OxGate Proxy

TNS proxy with authenticated pool.
Multiplex any OCI client.

The same crate ships OxGate: a TNS proxy that terminates the client's full O5LOGON handshake (including OCI 12.1 byte-perfect mimic) and bridges to a pool of pre-authenticated backend sessions. Clients connect normally; the database sees near-zero logon churn. Built on top of oracle-tns' own protocol implementation — no Instant Client, no DRCP licence, no CMAN.

Server-side O5LOGON termination
OxGate plays the role of an Oracle server during the client's auth: ACCEPT, NSP, TTIPRO/TTIDTY, AUTH_PHASE_ONE/TWO, AUTH_OK template — including OCI 12.1 and IC 11.2 byte-perfect mimic. What only libclntsh.so does normally.
Authenticated backend pool
Pre-opens authenticated backend sessions via a service account. Acquires from idle on the hot path; opens new sessions up to max_total under burst; replenishes idle in the background. Caps captured from the first connect, echoed to clients to prevent cap drift.
Multi-flavor wire support
Detects client flavor from CONNECT caps (sqlplus OCI 12.1, ODP.NET Unmanaged 12.1, ODP.NET Managed 23.x, JDBC Thin, ODP.NET Unmanaged + IC 11.2). Each flavor uses its own template set — TTIDTY catalog, AUTH_SESSKEY chunked encoding, framing width — so clients see exactly what their library expects.
Multi-pool with mixed dialects
A single listener can serve OCI 12.1 clients and IC 11.2 thick clients side by side. Per-user pools are configured with a use_oci_11 yaml flag selecting the backend handshake (proto::oci_mimic vs proto::oci_mimic_11). The bridge routes pool_map.get(user) after server-side auth so each client lands on a backend authenticated as the same user.
N → M multiplex (Phase 3b)
Statement-level swap: when a client closes a transaction and BridgeState reports is_safe_to_release(), the current backend session is returned to the pool and a fresh one is acquired for the next statement. N concurrent client connections multiplex onto M backend sessions where M ≪ N. Validated 20/20 mixed parallel against IC 11.2 + OCI 12.1 backends, zero auth_fail.
Idle-clean backend parking
Legacy clients (old Oracle drivers, client-side pools) keep TNS sessions open through user think-time. When a bridge is clean (is_safe_to_release()) and silent past OXGATE_IDLE_PARK_SECS, OxGate returns its backend to the pool and re-acquires one on the next client packet — re-running lb_ports rotation + quarantine. Many idle clients share few backends instead of pinning 1:1. Same safety gate as the swap; never closes or rolls back the session. Prod soak: parked 13/15 sessions, reuse 38% → 82%, zero ORA-01001/03137.
Try-multiple combo_key (O3LOGON)
ODP.NET Managed and JDBC Thin family share a wire fingerprint across versions but differ in combo_key derivation (PBKDF2-SHA512 vs MD5(server[16..32] XOR client[16..32])). The validator tries each candidate per flavor and accepts the first that yields valid PKCS7 padding AND a matching password — no logonCompatibility flag parsing, no version detection brittleness.
Transactional isolation on reuse
Before returning a session to the pool: rollback() + ping() under a 1.5s hard cap. Uncommitted DML from the prior client cannot leak. If cleanup times out, the session is discarded and the replenisher mints a fresh one.
Full session audit trail
Every session emits structured JSON events to the oxgate_audit stream: client_accept, auth_ok/auth_fail, backend_acquired/released, multiplex_swap, and the close — either client_logoff (clean LOGOFF) or client_disconnect with reason=client_eof | backend_eof | zombie_timeout. No invisible closures: any FIN on the proxy side is logged for SIEM/HIPAA.
Session identity enrichment
client_accept captures os_user and program (e.g. wsGestionVenta / w3wp.exe) from the CONNECT packet. A server-side map keyed by session_id then backfills those fields onto every later same-session event (backend_acquired/released, client_logoff) at read time — so the dashboard names the responsible app without a fragile client-side join.
Read-only in-flight observability
GET /v1/inflight lists every session currently holding (or parked) a backend with idle_secs (silence, not lifetime), tx_active, open_cursors, last statement class (never SQL text), and parked. GET /v1/clients rolls it up by (os_user, ip): who holds how many, how many idle-in-txn, over threshold. The proxy observes — it never kills or rolls back a client.
Long-idle + cursor-leak alerts
A background scanner fires long_inflight once per session that stays silent past OXGATE_INFLIGHT_ALERT_SECS (re-armed when traffic resumes — based on idle, so reused long-lived connections never false-alarm), and cursor_leak when a session holds ≥ OXGATE_CURSOR_LEAK_THRESHOLD open cursors (the ORA-01000 pattern). Both are read-only signals + WARN lines for ops; OxGate takes no action.
Listener load-balance with per-port quarantine
Round-robin every fresh backend connect across N Oracle listener ports (typically N RAC nodes or dispatcher groups). Each port tracks consecutive failures: after 3 fails in a row, that port is quarantined out of rotation for 30 s. Any single success clears the state. If every port is quarantined, fall back to the primary oracle_port — never out of options.
Reactive listener failover (post-PH1 mismatch)
If the primary listener hands off a TCP socket to a backend that is mid-OPI (DCB column metadata instead of AUTH_PHASE_ONE — observed in production 2026-05-25), the bridge fallback walks targets[*].failover_ports in order until one returns a valid AUTH. Audit listener_failover_recovered records every save. Distinct from load_balance_ports (active distribution).
TCP keepalive on client sockets
SO_KEEPALIVE 30 s / 10 s × 3 retries enabled in the accept() loop. Half-open clients (kill -9, NAT/VPN state lost, laptop suspend) are reaped in ~60 s instead of inflating /v1/apps active counters forever.
Mars mode — never falls
Lazy boot (Oracle unreachable on start), circuit breaker on backend connect, validate-on-acquire ping (catches dead idles), drain timeout on abrupt disconnect, acquire timeout on saturation. Every layer answers within bounded time.
Burst stress vs Oracle 12.1 SE — sqlplus 12.1 OCI clients
Concurrent clientsTotal reqsSuccessp50p99req/s
20200100%141 ms207 ms119
30600100%201 ms333 ms123
50500100%336 ms572 ms124
1300/1300
reqs OK during burst
0.1/s
logons (AWR confirmed)
~3.0 MB
static-pie musl binary
921 KB
RSS under 50 clients

Client Flavor Matrix

One listener,
five wire dialects.

Oracle clients across versions speak slightly different wire dialects. OxGate detects each from CONNECT capabilities and dispatches the right templates and crypto path. Pool reuse and Phase 3b multiplex work for every flavor that has a configured backend pool.

FlavorCaps (version / so / tdu)Typical clientCombo_keyPool / multiplex
SqlplusOci314 / 0x0c41 / 0x7fffsqlplus 12.1 OCIPBKDF2-SHA512 (client‖server)Yes
OdpUnmanaged314 / 0x0041 / 0x7fffODP.NET Unmanaged + IC 12.1MD5-XORYes
OdpManaged319 / 0x0c01 / —ODP.NET Managed 23.x, JDBC Thin 23+Try-multiple: PBKDF2 → MD5-XORYes
JdbcThin12315 / 0x0c41 / —HikariCP / ojdbc7 12.1PBKDF2-SHA512Open PH1 LOB bug
OdpUnmanagedOci314 / 0x0041 / 0xffffODP.NET Unmanaged + IC 11.2 (legacy thick)MD5-XOR (case 2361, ojdbc6 11.2)Yes (use_oci_11: true pool)
Legacy IC 11.2 thick path
Clients built on Oracle Instant Client 11.2 + ODP.NET Unmanaged speak a u16-framed wire with TTIDTY 2642 bytes, AUTH_SESSKEY encoded in chunked form (96 hex chars), and an MD5-XOR combo_key derivation that lives only in oci.dll. OxGate implements the algorithm from ojdbc6 11.2 case 2361 and dedicates a separate pool (use_oci_11: true) so backend sessions match the wire end-to-end. Phase 3b multiplex applies — both ends are u16, no reframe needed.
Why try-multiple combo_key
Within a flavor, clients across driver generations may pick a different combo_key algorithm (e.g. ODP.NET Managed 12.x uses MD5-XOR, 23.x uses PBKDF2-SHA512). The flag that distinguishes them lives in a PH1 KV pair OxGate does not parse. Instead, the validator tries each candidate per flavor and accepts the first whose AUTH_PASSWORD decrypt produces valid PKCS7 padding AND a matching password. Probability of false positive: under 2⁻¹²⁸.

Comparison

oracle-tns vs the rest

oracle-tnspython-oracledbnode-oracledbInstant Client
Binary size~5.5 MB (static musl)~50 MB + Python~30 MB + Node.js~250 MB
Instant Client requiredNoThin: No, Thick: YesThin: No, Thick: YesYes (is it)
Oracle 12c R1 supportYesFails (DPY-3015)FailsYes
10G/11G verifierYes (O3LOGON)NoNoYes
AsyncYes (tokio)Yes (asyncio)Yes (promises)No
Connection poolingYesYesYesYes (OCI)
Fuzz testedYes (48M+ execs)NoNoNo

Security

Fuzz-tested.
48 million executions.

Every parser in oracle-tns is fuzz-tested with cargo-fuzz. Five targets covering TNS packet decoding, number parsing, date decoding, codec operations, and connection packet handling. Five bugs found and fixed before they reached production.

BugTargetImpactStatus
decode_number overflowfuzz_numberPanic on malformed NUMBER bytesFixed
AcceptPacket boundsfuzz_tns_packetOut-of-bounds read on truncated packetFixed
codec OOMfuzz_codecAllocation bomb via crafted length prefix (DoS vector)Fixed
decode_date overflowfuzz_datePanic on invalid date componentsFixed
slice index panicfuzz_codecIndex out of bounds on partial CLR chunkFixed
5
fuzz targets
48M+
total executions
5
bugs found
0
open issues

Wire Protocol

TNS from the wire up.

TNS Packet Framing
Every Oracle message is wrapped in a TNS packet: 8-byte header with packet length, checksum, type, and flags. Types include Connect (1), Accept (2), Data (6), Marker (12). The driver handles packet reassembly, SDU negotiation, and chunked reads.
Variable-Length Integers
Oracle uses UB2 (2-byte) and UB4 (4-byte) unsigned integers in big-endian format for lengths and offsets. The driver reads these from the wire and uses them to parse field boundaries in data packets.
CLR Chunked Encoding
Column data is encoded in CLR (Column Length Representation) format: a length byte followed by data bytes. For values longer than 254 bytes, a chunked encoding is used with 0xFE prefix and multiple length-prefixed segments.
O3LOGON / O5LOGON Auth
O5LOGON uses a Diffie-Hellman key exchange with SHA-1. O3LOGON (verifier 0x939) is required for Oracle 12c R1 with legacy 10G/11G password verifiers — this is the auth that every other thin driver fails to implement.

API Reference

Key types and functions.

ConnectConfig
pub struct ConnectConfig {
    pub host: String,
    pub port: u16,
    pub service_name: String,
    pub username: String,
    pub password: String,
}
OracleSession
impl OracleSession {
    pub async fn connect(config: &ConnectConfig) -> Result<Self>;
    pub async fn query(&mut self, sql: &str, params: &[BindParam]) -> Result<Vec<Row>>;
    pub async fn execute(&mut self, sql: &str, params: &[BindParam]) -> Result<u64>;
    pub async fn call(&mut self, sql: &str, params: &mut [BindParam]) -> Result<()>;
    pub async fn ping(&mut self) -> Result<()>;
    pub async fn close(self) -> Result<()>;
}
BindParam
impl BindParam {
    pub fn varchar(value: &str) -> Self;
    pub fn integer(value: i64) -> Self;
    pub fn out_varchar() -> Self;
    pub fn out_cursor() -> Self;
}
Cursor + OracleValue
impl Cursor {
    pub fn fetch_all(&self) -> &[Row];
    pub fn columns(&self) -> &[ColumnInfo];
}

pub enum OracleValue {
    Varchar(String),
    Number(f64),
    Integer(i64),
    Date(NaiveDateTime),
    Raw(Vec<u8>),
    Null,
}