oracle-tns
Documentation

oracle-tns

oracle-tns is a pure Rust implementation of the Oracle TNS wire protocol. It connects directly to Oracle databases over TCP without requiring Oracle Instant Client, OCI, or any C dependencies. It is the first open-source thin driver that successfully authenticates against Oracle 12c R1 instances using legacy 10G/11G password verifiers (O3LOGON with verifier 0x939).


Installation

Add oracle-tns to your Cargo.toml:

Cargo
cargo add oracle-tns

Or manually in your Cargo.toml:

Cargo.toml
[dependencies]
oracle-tns = "0.1"

oracle-tns depends on tokio for async I/O. Make sure you have tokio in your dependencies with the rt-multi-thread and macros features enabled.


Connection Config

Create a ConnectConfig with your Oracle connection details. The driver connects via TCP to the specified host and port, then performs TNS handshake and authentication.

Connection setup
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(),
};

let mut session = OracleSession::connect(&config).await?;

// Verify the connection is alive
session.ping().await?;

// When done
session.close().await?;

The driver automatically detects the server's authentication method and selects O5LOGON or O3LOGON accordingly. For Oracle 12c R1 with 10G/11G password verifiers, O3LOGON (verifier 0x939) is used — this is the auth path that other thin drivers fail on.


Basic Queries

Use session.query() for SELECT statements. Rows are returned as a Vec<Row> where each row provides typed access to column values.

Simple SELECT
let rows = session.query(
    "SELECT id, name, email, created_at FROM users",
    &[],
).await?;

for row in &rows {
    let id: i64 = row.get("ID")?;
    let name: String = row.get("NAME")?;
    let email: String = row.get("EMAIL")?;
    println!("{id}: {name} <{email}>");
}
Working with OracleValue
// Access raw OracleValue for nullable fields
let rows = session.query("SELECT name, phone FROM contacts", &[]).await?;

for row in &rows {
    let name = row.get_value("NAME")?;
    let phone = row.get_value("PHONE")?;

    match phone {
        OracleValue::Varchar(p) => println!("{name}: {p}"),
        OracleValue::Null => println!("{name}: no phone"),
        _ => {}
    }
}

Bind Parameters

Use positional bind variables (:1, :2, etc.) to pass parameters safely. This prevents SQL injection and allows Oracle to reuse query plans.

Bind parameters
use oracle_tns::BindParam;

// VARCHAR bind
let rows = session.query(
    "SELECT * FROM employees WHERE department = :1 AND status = :2",
    &[
        BindParam::varchar("Engineering"),
        BindParam::varchar("active"),
    ],
).await?;

// INTEGER bind
let rows = session.query(
    "SELECT * FROM orders WHERE amount > :1 AND customer_id = :2",
    &[
        BindParam::integer(1000),
        BindParam::integer(42),
    ],
).await?;

Pagination

Oracle 12c+ supports OFFSET/FETCH NEXT for row limiting. Use bind parameters for the offset and fetch count.

OFFSET/FETCH pagination
// Page 1: rows 1-20
let page1 = session.query(
    "SELECT id, name FROM products ORDER BY id      OFFSET :1 ROWS FETCH NEXT :2 ROWS ONLY",
    &[BindParam::integer(0), BindParam::integer(20)],
).await?;

// Page 2: rows 21-40
let page2 = session.query(
    "SELECT id, name FROM products ORDER BY id      OFFSET :1 ROWS FETCH NEXT :2 ROWS ONLY",
    &[BindParam::integer(20), BindParam::integer(20)],
).await?;

PL/SQL Blocks

Execute anonymous PL/SQL blocks for complex server-side logic.

Anonymous PL/SQL block
session.execute(
    "BEGIN
        UPDATE accounts SET balance = balance - :1 WHERE id = :2;
        UPDATE accounts SET balance = balance + :1 WHERE id = :3;
        COMMIT;
     END;",
    &[
        BindParam::integer(500),
        BindParam::integer(1001),  // from account
        BindParam::integer(1002),  // to account
    ],
).await?;

Stored Procedures with OUT Params

Call stored procedures and read output parameters. OUT parameters are declared with BindParam::out_varchar() and read back after the call completes.

Calling a stored procedure
// Procedure: GET_USER_INFO(p_id IN NUMBER, p_name OUT VARCHAR2, p_email OUT VARCHAR2)
let mut params = vec![
    BindParam::integer(42),
    BindParam::out_varchar(),
    BindParam::out_varchar(),
];

session.call(
    "BEGIN GET_USER_INFO(:1, :2, :3); END;",
    &mut params,
).await?;

// Read OUT values
let name = params[1].as_varchar().unwrap_or_default();
let email = params[2].as_varchar().unwrap_or_default();
println!("User: {name} <{email}>");

REF CURSOR

Bind an OUT cursor to receive a result set from a stored procedure. The cursor provides column metadata and all rows.

REF CURSOR example
// Procedure: GET_DEPT_EMPLOYEES(p_dept IN VARCHAR2, p_cursor OUT SYS_REFCURSOR)
let mut params = vec![
    BindParam::varchar("Engineering"),
    BindParam::out_cursor(),
];

session.call(
    "BEGIN GET_DEPT_EMPLOYEES(:1, :2); END;",
    &mut params,
).await?;

// Extract the cursor
let cursor = params[1].as_cursor()?;

// Column metadata
for col in cursor.columns() {
    println!("Column: {} ({})", col.name, col.data_type);
}

// Fetch all rows
for row in cursor.fetch_all() {
    let id: i64 = row.get("EMPLOYEE_ID")?;
    let name: String = row.get("EMPLOYEE_NAME")?;
    println!("{id}: {name}");
}

DML Operations

Use session.execute() for INSERT, UPDATE, DELETE, and DDL statements. Returns the number of affected rows.

INSERT / UPDATE / DELETE
// INSERT
let affected = session.execute(
    "INSERT INTO users (id, name, email) VALUES (:1, :2, :3)",
    &[
        BindParam::integer(100),
        BindParam::varchar("Alice"),
        BindParam::varchar("alice@example.com"),
    ],
).await?;
println!("Inserted {affected} row(s)");

// UPDATE
let affected = session.execute(
    "UPDATE users SET email = :1 WHERE id = :2",
    &[
        BindParam::varchar("newemail@example.com"),
        BindParam::integer(100),
    ],
).await?;

// DELETE
let affected = session.execute(
    "DELETE FROM users WHERE id = :1",
    &[BindParam::integer(100)],
).await?;
DDL (CREATE / DROP)
// CREATE TABLE
session.execute(
    "CREATE TABLE temp_data (
        id NUMBER PRIMARY KEY,
        value VARCHAR2(200),
        created_at DATE DEFAULT SYSDATE
    )",
    &[],
).await?;

// DROP TABLE
session.execute("DROP TABLE temp_data", &[]).await?;

Connection Pooling

oracle-tns provides an async connection pool for managing multiple concurrent connections. Configure minimum/maximum pool size and idle timeout.

Connection pool
use oracle_tns::{ConnectConfig, ConnectionPool, PoolConfig};

let config = ConnectConfig {
    host: "db.example.com".to_string(),
    port: 1521,
    service_name: "MYDB".to_string(),
    username: "myuser".to_string(),
    password: "mypassword".to_string(),
};

let pool_config = PoolConfig {
    min_connections: 2,
    max_connections: 10,
    idle_timeout_secs: 300,
};

let pool = ConnectionPool::new(config, pool_config).await?;

// Acquire a connection from the pool
let mut conn = pool.acquire().await?;

let rows = conn.query("SELECT COUNT(*) as cnt FROM users", &[]).await?;

// Connection is returned to the pool when dropped
drop(conn);

// Shut down the pool
pool.close().await?;

Error Handling

All operations return Result<T, OracleError>. Errors are categorized by type for precise handling.

Error handling
use oracle_tns::OracleError;

match session.query("SELECT * FROM nonexistent_table", &[]).await {
    Ok(rows) => {
        println!("Got {} rows", rows.len());
    }
    Err(OracleError::ServerError { code, message }) => {
        // ORA-00942: table or view does not exist
        eprintln!("Oracle error {code}: {message}");
    }
    Err(OracleError::ConnectionError(e)) => {
        eprintln!("Connection lost: {e}");
    }
    Err(OracleError::AuthError(e)) => {
        eprintln!("Authentication failed: {e}");
    }
    Err(e) => {
        eprintln!("Unexpected error: {e}");
    }
}

Wire Protocol Internals

oracle-tns implements the Oracle TNS protocol from scratch. Understanding the wire format helps when debugging connection issues or contributing to the driver.

TNS Packet Structure

Every Oracle message is wrapped in a TNS packet with an 8-byte header:

TNS packet header (8 bytes)
Offset  Size  Field
0       2     Packet length (UB2, big-endian)
2       2     Packet checksum
4       1     Packet type (1=Connect, 2=Accept, 4=Refuse, 6=Data, 12=Marker)
5       1     Reserved
6       2     Header checksum

Data Types on the Wire

Oracle encodes column data using CLR (Column Length Representation) format. A single byte indicates the length, followed by that many data bytes. For values longer than 254 bytes, a chunked encoding is used:

CLR encoding
Short value (len <= 254):
  [len] [data bytes...]

Long value (chunked, len > 254):
  [0xFE] [chunk1_len] [chunk1_data...] [chunk2_len] [chunk2_data...] ... [0x00]

NULL value:
  [0x00]

Authentication Flow

The connection and authentication sequence:

Auth flow
1. TCP connect to host:port
2. Send TNS Connect packet (with connect string)
3. Receive TNS Accept packet (SDU negotiation)
4. Send protocol negotiation (NSPTCN)
5. Send data types negotiation (NSPTDA)
6. Send auth request (username + AUTH_SESSKEY)
7. Receive server auth challenge (session key, VFR data, verifier type)
8. Compute auth response:
   - O5LOGON: DH key exchange + SHA-1 password hash
   - O3LOGON: legacy verifier 0x939 for 10G/11G passwords
9. Send auth response
10. Receive auth confirmation
11. Session established — ready for queries

DALC Fields

Oracle uses DALC (Data Access Layer Communication) fields in the data payload. These are type-length-value structures that carry query results, error codes, column metadata, and control messages. The driver parses these to extract row data and server responses.


Security (Fuzzing)

Every parser in oracle-tns is fuzz-tested with cargo-fuzz. The fuzzing campaign has run 48 million+ executions across five targets and found five bugs — all fixed before reaching production.

Fuzz Targets

fuzz_tns_packetTNS packet parsing — Connect, Accept, Data, Marker packets with random payloads
fuzz_numberOracle NUMBER decoding — variable-length numeric format with sign, exponent, mantissa
fuzz_dateOracle DATE decoding — 7-byte date format with century, year, month, day, hour, minute, second
fuzz_codecFull codec pipeline — CLR decoding, chunked reads, field boundary parsing
fuzz_connectConnection packet handling — TNS handshake parsing with malformed packets

Bugs Found

BugTargetDescription
decode_number overflowfuzz_numberInteger overflow when parsing mantissa bytes with extreme exponent values. Fixed with checked arithmetic.
AcceptPacket boundsfuzz_tns_packetOut-of-bounds read when Accept packet was shorter than expected header. Fixed with length validation before slice access.
codec OOMfuzz_codecCrafted length prefix caused multi-GB allocation attempt (DoS vector). Fixed with max allocation limit of 16 MB.
decode_date overflowfuzz_dateSubtraction underflow on century byte when value was 0. Fixed with saturating subtraction.
slice index panicfuzz_codecIndex out of bounds when CLR chunk indicated more data than available. Fixed with bounds check before slice.
Running the fuzz tests
# Install cargo-fuzz
cargo install cargo-fuzz

# Run a specific target
cargo fuzz run fuzz_tns_packet -- -max_len=65536

# Run all targets
for target in fuzz_tns_packet fuzz_number fuzz_date fuzz_codec fuzz_connect; do
    cargo fuzz run $target -- -max_len=65536 -runs=1000000 &
done
wait

API Reference

ConnectConfig

ConnectConfig
pub struct ConnectConfig {
    /// Oracle host (IP or hostname)
    pub host: String,
    /// TNS listener port (default 1521)
    pub port: u16,
    /// Oracle service name (e.g. "ORCL", "MYDB")
    pub service_name: String,
    /// Database username
    pub username: String,
    /// Database password
    pub password: String,
}

OracleSession

OracleSession methods
impl OracleSession {
    /// Connect to Oracle and authenticate.
    /// Automatically selects O5LOGON or O3LOGON based on server capabilities.
    pub async fn connect(config: &ConnectConfig) -> Result<Self, OracleError>;

    /// Execute a SELECT query with bind parameters.
    /// Returns all result rows.
    pub async fn query(
        &mut self,
        sql: &str,
        params: &[BindParam],
    ) -> Result<Vec<Row>, OracleError>;

    /// Execute a DML/DDL statement.
    /// Returns the number of affected rows.
    pub async fn execute(
        &mut self,
        sql: &str,
        params: &[BindParam],
    ) -> Result<u64, OracleError>;

    /// Call a PL/SQL block with mutable bind parameters (for OUT params).
    pub async fn call(
        &mut self,
        sql: &str,
        params: &mut [BindParam],
    ) -> Result<(), OracleError>;

    /// Send a ping to verify the connection is alive.
    pub async fn ping(&mut self) -> Result<(), OracleError>;

    /// Close the session and TCP connection gracefully.
    pub async fn close(self) -> Result<(), OracleError>;
}

BindParam

BindParam constructors
impl BindParam {
    /// Create a VARCHAR2 input parameter.
    pub fn varchar(value: &str) -> Self;

    /// Create a NUMBER/INTEGER input parameter.
    pub fn integer(value: i64) -> Self;

    /// Create a VARCHAR2 OUT parameter (for stored procedures).
    pub fn out_varchar() -> Self;

    /// Create a SYS_REFCURSOR OUT parameter.
    pub fn out_cursor() -> Self;

    /// Read the output value as a string (after call()).
    pub fn as_varchar(&self) -> Option<&str>;

    /// Read the output value as a Cursor (after call()).
    pub fn as_cursor(&self) -> Result<&Cursor, OracleError>;
}

Row and OracleValue

Row access + OracleValue enum
impl Row {
    /// Get a typed value by column name (case-insensitive).
    pub fn get<T: FromOracleValue>(&self, column: &str) -> Result<T, OracleError>;

    /// Get the raw OracleValue by column name.
    pub fn get_value(&self, column: &str) -> Result<&OracleValue, OracleError>;
}

/// Column value types returned by Oracle.
pub enum OracleValue {
    Varchar(String),
    Number(f64),
    Integer(i64),
    Date(NaiveDateTime),
    Raw(Vec<u8>),
    Null,
}

impl Cursor {
    /// Get all rows from the REF CURSOR result set.
    pub fn fetch_all(&self) -> &[Row];

    /// Get column metadata (name, data type, size).
    pub fn columns(&self) -> &[ColumnInfo];
}

pub struct ColumnInfo {
    pub name: String,
    pub data_type: String,
    pub size: u32,
}

ConnectionPool

ConnectionPool
pub struct PoolConfig {
    pub min_connections: usize,
    pub max_connections: usize,
    pub idle_timeout_secs: u64,
}

impl ConnectionPool {
    /// Create a new pool and establish min_connections.
    pub async fn new(
        config: ConnectConfig,
        pool_config: PoolConfig,
    ) -> Result<Self, OracleError>;

    /// Acquire a connection from the pool (waits if all are in use).
    pub async fn acquire(&self) -> Result<PooledConnection, OracleError>;

    /// Shut down the pool and close all connections.
    pub async fn close(&self) -> Result<(), OracleError>;
}

OracleError

OracleError variants
pub enum OracleError {
    /// Oracle server error (ORA-XXXXX).
    ServerError { code: i32, message: String },

    /// TCP/IO connection error.
    ConnectionError(String),

    /// Authentication failure (wrong credentials, unsupported verifier).
    AuthError(String),

    /// TNS protocol error (malformed packet, unexpected response).
    ProtocolError(String),

    /// Pool error (timeout acquiring connection, pool closed).
    PoolError(String),

    /// Column not found or type conversion error.
    DataError(String),
}

Known Limitations

CLOB/BLOB streaming
Large object types are not yet supported. Values over the SDU size (typically 8192 bytes) will need streaming reads with multiple round-trips. Implementation pending.
TIMESTAMP WITH TIME ZONE
Basic DATE (7-byte) and TIMESTAMP work correctly. TIMESTAMP WITH TIME ZONE and TIMESTAMP WITH LOCAL TIME ZONE have additional timezone region/offset bytes that are not yet parsed.
TCP keepalive
Long-lived connections over unreliable networks (VPN, cloud) may drop silently after idle periods. TCP keepalive socket options are not yet configured. Workaround: use ping() periodically or use the connection pool with idle_timeout_secs.
SSL/TLS (TCPS)
The driver connects over plain TCP. Oracle TCPS (TLS-encrypted connections) are not yet supported. For encrypted transport, use an SSH tunnel or VPN.
Oracle RAC (TAF/FAN)
Transparent Application Failover and Fast Application Notification for RAC clusters are not implemented. The driver connects to a single host:port.