oracle-tns
Driver Oracle

Sin Instant Client.
Rust puro. Oracle 12c+.

oracle-tns es una implementacion en Rust puro del protocolo TNS de Oracle. Sin Oracle Instant Client, sin bindings a C, sin bibliotecas compartidas. Un binario estatico musl que habla TNS nativamente — O3LOGON, O5LOGON, queries, PL/SQL, procedimientos almacenados, REF CURSORs.

Este driver conecta a Oracle 12c R1 con verificadores de contrasena 10G/11G — algo que python-oracledb, node-oracledb, oracle-nio y kalliope no pueden hacer (DPY-3015).
~5.5 MB
binario estatico musl
13ms
latencia promedio VPN
5
targets de fuzzing
48M+
ejecuciones de fuzz

Inicio Rapido

Conectar a Oracle
en tres pasos.

1. Agregar dependencia
cargo add oracle-tns
2. Configurar conexion
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. Consultar
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?;

Funcionalidades

Protocolo Oracle completo
en Rust puro.

ConnectHandshake TNS, autenticacion O3LOGON (0x939) + O5LOGON, resolucion de service name
QuerySELECT con decodificacion completa — VARCHAR2, NUMBER, DATE, RAW, NULL
Bind parametersVariables de bind posicionales (:1, :2) — tipos VARCHAR, INTEGER
PaginacionSoporte OFFSET/FETCH NEXT para limitacion de filas en Oracle 12c+
Bloques PL/SQLEjecucion de bloques PL/SQL anonimos con variables de bind
ProcedimientosCALL con parametros IN y OUT
Parametros OUTLectura de valores de salida de llamadas a procedimientos
REF CURSORBind de cursor OUT, fetch de result set con metadata de columnas
DMLINSERT, UPDATE, DELETE, CREATE TABLE, DROP TABLE
Connection poolPool asincrono con conexiones min/max configurables y timeout de inactividad
PingVerificacion ligera de salud de conexion sobre TNS
Proxy OxGateProxy TNS con pool de backends autenticados y mimic byte-perfect OCI 12.1 — ver seccion abajo

Proxy OxGate

Proxy TNS con pool autenticado.
Multiplex para cualquier cliente OCI.

El mismo crate incluye OxGate: un proxy TNS que termina el handshake O5LOGON completo del cliente (incluido el mimic byte-perfect de OCI 12.1) y bridge a un pool de sesiones backend pre-autenticadas. Los clientes conectan normal; la base ve casi cero churn de logons. Construido sobre la implementacion de protocolo de oracle-tns — sin Instant Client, sin licencia DRCP, sin CMAN.

Terminacion O5LOGON server-side
OxGate juega el rol de un servidor Oracle durante la autenticacion del cliente: ACCEPT, NSP, TTIPRO/TTIDTY, AUTH_PHASE_ONE/TWO, AUTH_OK — con mimic byte-perfect tanto de OCI 12.1 como de IC 11.2. Lo que solo libclntsh.so hace normalmente.
Pool backend autenticado
Pre-abre sesiones backend autenticadas via service account. Toma de idle en hot path; abre nuevas hasta max_total bajo rafaga; reabastece idle en background. Caps capturadas del primer connect, ecoadas a los clientes para evitar drift.
Soporte multi-flavor del wire
Detecta el flavor del cliente desde las caps del CONNECT (sqlplus OCI 12.1, ODP.NET Unmanaged 12.1, ODP.NET Managed 23.x, JDBC Thin, ODP.NET Unmanaged + IC 11.2). Cada flavor usa su propio set de templates — catalogo TTIDTY, encoding chunked de AUTH_SESSKEY, ancho de framing — para que el cliente reciba exactamente lo que su libreria espera.
Multi-pool con dialectos mezclados
Un mismo listener puede servir clientes OCI 12.1 y clientes IC 11.2 thick lado a lado. Los pools por usuario se configuran con un flag yaml use_oci_11 que selecciona el handshake del backend (proto::oci_mimic vs proto::oci_mimic_11). El bridge enruta pool_map.get(user) tras la auth server-side, asi cada cliente cae en una conn backend autenticada como su mismo usuario.
Multiplex N → M (Phase 3b)
Swap a nivel de statement: cuando un cliente cierra una transaccion y BridgeState reporta is_safe_to_release(), la sesion backend actual vuelve al pool y se acquire una fresca para el siguiente statement. N clientes logicos multiplexan sobre M sesiones backend con M ≪ N. Validado 20/20 mixed paralelos contra IC 11.2 + OCI 12.1, cero auth_fail.
Parking de backend idle-limpio
Los clientes legacy (drivers Oracle viejos, pools client-side) mantienen sesiones TNS abiertas durante el think-time del usuario. Cuando un bridge esta limpio (is_safe_to_release()) y en silencio mas alla de OXGATE_IDLE_PARK_SECS, OxGate devuelve su backend al pool y re-adquiere uno en el proximo paquete — re-corriendo la rotacion lb_ports + cuarentena. Muchos clientes idle comparten pocos backends en vez de pinear 1:1. Mismo gate de seguridad que el swap; nunca cierra ni hace rollback. Soak en prod: 13/15 sesiones parqueadas, reuse 38% → 82%, cero ORA-01001/03137.
Combo_key try-multiple (O3LOGON)
ODP.NET Managed y la familia JDBC Thin comparten fingerprint del wire entre versiones pero difieren en la derivacion del combo_key (PBKDF2-SHA512 vs MD5(server[16..32] XOR client[16..32])). El validador prueba cada candidato por flavor y acepta el primero que produce padding PKCS7 valido Y password match — sin parsear el flag logonCompatibility, sin frialdad por deteccion de version.
Aislamiento transaccional al reusar
Antes de devolver una sesion al pool: rollback() + ping() bajo cap duro de 1.5s. DML no commiteado del cliente anterior no leakea. Si el cleanup expira, la sesion se descarta y el replenisher mintea una fresca.
Audit trail de sesion completo
Cada sesion emite eventos JSON estructurados al stream oxgate_audit: client_accept, auth_ok/auth_fail, backend_acquired/released, multiplex_swap, y el cierre — sea client_logoff (LOGOFF limpio) o client_disconnect con reason=client_eof | backend_eof | zombie_timeout. Cero cierres invisibles: cualquier FIN del lado proxy queda registrado para SIEM/HIPAA.
Enriquecimiento de identidad de sesion
client_accept captura os_user y program (ej. wsGestionVenta / w3wp.exe) del paquete CONNECT. Un mapa server-side por session_id rellena esos campos en cada evento posterior de la misma sesion (backend_acquired/released, client_logoff) en tiempo de lectura — la consola nombra a la app responsable sin un join fragil del lado cliente.
Observabilidad in-flight read-only
GET /v1/inflight lista cada sesion que tiene (o parquea) un backend con idle_secs (silencio, no tiempo de vida), tx_active, open_cursors, clase del ultimo statement (nunca texto SQL) y parked. GET /v1/clients lo agrega por (os_user, ip): quien retiene cuantas, cuantas idle-in-txn, sobre umbral. El proxy observa — nunca mata ni hace rollback a un cliente.
Alertas de idle-largo + cursor-leak
Un scanner en background emite long_inflight una vez por sesion que queda en silencio mas alla de OXGATE_INFLIGHT_ALERT_SECS (re-armado cuando vuelve el trafico — basado en idle, asi las conexiones reusadas de larga vida no dan falsas alarmas), y cursor_leak cuando una sesion tiene ≥ OXGATE_CURSOR_LEAK_THRESHOLD cursores abiertos (el patron ORA-01000). Ambas son señales read-only + lineas WARN para ops; OxGate no toma accion.
Load-balance de listeners con cuarentena por puerto
Round-robin de cada handshake nuevo entre N listeners Oracle (tipicamente N nodos RAC o dispatcher groups). Cada puerto trackea fallos consecutivos: tras 3 fails en linea, ese puerto sale de rotacion 30 s. Cualquier success limpia el estado. Si todos quedan en cuarentena, fallback al primary oracle_port — modo marte: nunca sin opciones.
Failover reactivo de listener (post-PH1 mismatch)
Si el listener primario entrega un socket TCP a un backend mid-OPI (responde DCB column metadata en lugar de AUTH_PHASE_ONE — observado en produccion 2026-05-25), el bridge recorre targets[*].failover_ports hasta encontrar uno que devuelva AUTH valido. Audit listener_failover_recovered queda registrado en cada save. Distinto de load_balance_ports (distribucion activa).
TCP keepalive en sockets cliente
SO_KEEPALIVE 30 s / 10 s × 3 retries activado en el accept() loop. Half-open clients (kill -9, NAT/VPN sin estado, laptop suspendida) se reapean en ~60 s en vez de inflar contadores /v1/apps active para siempre.
Modo Marte — nunca cae
Lazy boot (Oracle inalcanzable al arrancar), circuit breaker en backend connect, ping de validate-on-acquire (atrapa idles muertos), drain timeout en desconexion abrupta, acquire timeout en saturacion. Cada capa responde en tiempo acotado.
Stress de rafaga vs Oracle 12.1 SE — clientes sqlplus 12.1 OCI
Clientes concurrentesReqs totalesExitop50p99req/s
20200100%141 ms207 ms119
30600100%201 ms333 ms123
50500100%336 ms572 ms124
1300/1300
reqs OK en rafaga
0.1/s
logons (AWR confirmado)
~3.0 MB
binario static-pie musl
921 KB
RSS con 50 clientes

Matriz de flavors de cliente

Un listener,
cinco dialectos del wire.

Los clientes Oracle de distintas versiones hablan dialectos ligeramente distintos del wire. OxGate detecta cada uno desde las capabilities del CONNECT y dispara los templates y la ruta cripto correctos. Pool reuse y multiplex Phase 3b funcionan para todo flavor que tenga un pool backend configurado.

FlavorCaps (version / so / tdu)Cliente tipicoCombo_keyPool / multiplex
SqlplusOci314 / 0x0c41 / 0x7fffsqlplus 12.1 OCIPBKDF2-SHA512 (client‖server)Si
OdpUnmanaged314 / 0x0041 / 0x7fffODP.NET Unmanaged + IC 12.1MD5-XORSi
OdpManaged319 / 0x0c01 / —ODP.NET Managed 23.x, JDBC Thin 23+Try-multiple: PBKDF2 → MD5-XORSi
JdbcThin12315 / 0x0c41 / —HikariCP / ojdbc7 12.1PBKDF2-SHA512Bug abierto en PH1 LOB
OdpUnmanagedOci314 / 0x0041 / 0xffffODP.NET Unmanaged + IC 11.2 (legacy thick)MD5-XOR (case 2361, ojdbc6 11.2)Si (pool con use_oci_11: true)
Path legacy IC 11.2 thick
Los clientes construidos sobre Oracle Instant Client 11.2 + ODP.NET Unmanaged hablan un wire framed en u16, con TTIDTY de 2642 bytes, AUTH_SESSKEY encodeado en forma chunked (96 hex chars) y derivacion de combo_key MD5-XOR que vive solo en oci.dll. OxGate implementa el algoritmo de ojdbc6 11.2 case 2361 y dedica un pool aparte (use_oci_11: true) para que las sesiones backend matcheen el wire end-to-end. Multiplex Phase 3b aplica — ambos extremos son u16, no hay reframe.
Por que combo_key try-multiple
Dentro de un mismo flavor, los clientes pueden elegir un algoritmo distinto de combo_key entre generaciones del driver (ODP.NET Managed 12.x usa MD5-XOR, 23.x usa PBKDF2-SHA512). El flag que los distingue vive en un KV pair del PH1 que OxGate no parsea. En cambio, el validador prueba cada candidato del flavor y acepta el primero cuyo decrypt de AUTH_PASSWORD produce padding PKCS7 valido Y password match. Probabilidad de falso positivo: bajo 2⁻¹²⁸.

Comparacion

oracle-tns vs el resto

oracle-tnspython-oracledbnode-oracledbInstant Client
Tamano binario~5.5 MB (static musl)~50 MB + Python~30 MB + Node.js~250 MB
Requiere Instant ClientNoThin: No, Thick: SiThin: No, Thick: SiSi (lo es)
Soporte Oracle 12c R1SiFalla (DPY-3015)FallaSi
Verificador 10G/11GSi (O3LOGON)NoNoSi
AsincronoSi (tokio)Si (asyncio)Si (promises)No
Connection poolingSiSiSiSi (OCI)
Fuzz testingSi (48M+ ejecuciones)NoNoNo

Seguridad

Fuzz-tested.
48 millones de ejecuciones.

Cada parser en oracle-tns esta fuzz-tested con cargo-fuzz. Cinco targets cubriendo decodificacion de paquetes TNS, parsing de numeros, decodificacion de fechas, operaciones de codec y manejo de paquetes de conexion. Cinco bugs encontrados y corregidos antes de llegar a produccion.

BugTargetImpactoEstado
decode_number overflowfuzz_numberPanic con bytes NUMBER malformadosCorregido
AcceptPacket boundsfuzz_tns_packetLectura fuera de limites en paquete truncadoCorregido
codec OOMfuzz_codecBomba de asignacion via prefijo de longitud (vector DoS)Corregido
decode_date overflowfuzz_datePanic con componentes de fecha invalidosCorregido
slice index panicfuzz_codecIndice fuera de limites en chunk CLR parcialCorregido
5
targets de fuzz
48M+
ejecuciones totales
5
bugs encontrados
0
issues abiertos

Protocolo Wire

TNS desde el cable.

TNS Packet Framing
Cada mensaje Oracle se envuelve en un paquete TNS: header de 8 bytes con longitud, checksum, tipo y flags. Los tipos incluyen Connect (1), Accept (2), Data (6), Marker (12). El driver maneja reensamblado de paquetes, negociacion SDU y lecturas chunked.
Enteros de Longitud Variable
Oracle usa enteros sin signo UB2 (2 bytes) y UB4 (4 bytes) en formato big-endian para longitudes y offsets. El driver lee estos del cable y los usa para parsear limites de campos en paquetes de datos.
CLR Chunked Encoding
Los datos de columna se codifican en formato CLR (Column Length Representation): un byte de longitud seguido de bytes de datos. Para valores mayores a 254 bytes, se usa codificacion chunked con prefijo 0xFE y multiples segmentos con prefijo de longitud.
Autenticacion O3LOGON / O5LOGON
O5LOGON usa intercambio de claves Diffie-Hellman con SHA-1. O3LOGON (verificador 0x939) es requerido para Oracle 12c R1 con verificadores legacy 10G/11G — esta es la autenticacion que todos los otros thin drivers fallan en implementar.

Referencia API

Tipos y funciones principales.

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,
}