Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"crypto/tls"
"database/sql/driver"
"fmt"
"net/http"
"net/url"
"regexp"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/databricks/databricks-sql-go/auth/pat"
"github.com/databricks/databricks-sql-go/auth/tokenprovider"
"github.com/databricks/databricks-sql-go/driverctx"
dbsqlerr "github.com/databricks/databricks-sql-go/errors"
"github.com/databricks/databricks-sql-go/internal/backend"
"github.com/databricks/databricks-sql-go/internal/backend/thrift"
"github.com/databricks/databricks-sql-go/internal/client"
Expand Down Expand Up @@ -43,6 +45,16 @@ func (c *connector) Connect(ctx context.Context) (driver.Conn, error) {
if c.cfg.UseKernel {
be, err = newKernelBackend(ctx, c.cfg)
} else {
// The experimental WithKernel* TLS options have no Thrift-path equivalent —
// reject them loudly rather than silently ignore, so a caller who sets a
// trusted-CA bundle / an independent hostname skip and forgets WithUseKernel
// learns the option had no effect instead of connecting with a
// weaker-than-intended (or unconfigured) TLS trust store.
if c.cfg.KernelExperimental != nil {
return nil, fmt.Errorf("databricks: a WithKernel* option "+
"(WithKernelTrustedCerts / WithKernelSkipHostnameVerify) %w; "+
"add WithUseKernel(true) or remove it", dbsqlerr.ErrRequiresKernelBackend)
}
be, err = thrift.New(ctx, c.cfg, c.client)
}
if err != nil {
Expand Down Expand Up @@ -537,3 +549,57 @@ func WithFederatedTokenProviderAndClientID(baseProvider tokenprovider.TokenProvi
}
}
}

// ─── Experimental kernel-only options ─────────────────────────────────────────
//
// These configure the SEA-via-kernel backend (WithUseKernel) only; they expose a
// richer TLS surface than the backend-neutral WithSkipTLSHostVerify. They have no
// equivalent on the default (Thrift) path, which rejects them loudly at connect.
// They are deliberately NOT part of the stable DSN/UserConfig surface — they hang
// off config.Config.KernelExperimental (mirroring Node's non-exported
// InternalConnectionOptions). The WithKernel* prefix signals both "kernel-backend
// only" and "experimental" so they read distinctly from the backend-neutral
// options above (e.g. WithSkipTLSHostVerify).

// kernelExperimental lazily allocates and returns the experimental config block.
func kernelExperimental(c *config.Config) *config.KernelExperimentalConfig {
if c.KernelExperimental == nil {
c.KernelExperimental = &config.KernelExperimentalConfig{}
}
return c.KernelExperimental
}

// WithKernelTrustedCerts adds a PEM CA-certificate bundle to the kernel's TLS
// trust store on top of the system roots — for a corporate re-signing proxy or an
// on-prem CA. Required (rather than relying on SSL_CERT_FILE) because the kernel's
// rustls stack does not read that environment variable.
//
// EXPERIMENTAL, kernel-only: the default (Thrift) backend rejects this at connect.
func WithKernelTrustedCerts(pem []byte) ConnOption {
return func(c *config.Config) {
// Copy defensively (matching KernelExperimentalConfig.DeepCopy) so a
// caller mutating pem between NewConnector and Connect can't change the
// trust store out from under us.
if len(pem) > 0 {
kernelExperimental(c).TLSTrustedCertsPEM = append([]byte(nil), pem...)
} else {
kernelExperimental(c).TLSTrustedCertsPEM = pem
}
}
}

// WithKernelSkipHostnameVerify skips only the certificate hostname-vs-SNI check on
// the kernel backend, while keeping chain validation. This is finer-grained than
// WithSkipTLSHostVerify, which relaxes both chain and hostname checks.
// WARNING:
// Skipping hostname verification still weakens TLS: a certificate issued by a
// trusted CA for a different host will be accepted, opening a machine-in-the-middle
// vector. Only use this when the hostname is an internal private-link hostname that
// legitimately differs from the certificate's subject.
//
// EXPERIMENTAL, kernel-only: the default (Thrift) backend rejects this at connect.
func WithKernelSkipHostnameVerify() ConnOption {
Comment thread
mani-mathur-arch marked this conversation as resolved.
return func(c *config.Config) {
kernelExperimental(c).TLSSkipHostnameVerify = true
}
}
22 changes: 22 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,23 @@ WithMaxRows and retry tuning (WithRetries with a positive limit) are accepted bu
not applied on the kernel path: the kernel manages result fetching and retries
internally, below the C ABI, with no user-facing knob.

Experimental kernel-only TLS options. The kernel exposes a richer TLS surface than
the backend-neutral WithSkipTLSHostVerify (which relaxes both chain and hostname
checks). These are kernel-only — the default (Thrift) backend rejects them at
connect rather than silently ignoring them — and experimental (the WithKernel*
prefix marks both):
- WithKernelTrustedCerts(pem) adds a PEM CA bundle to the kernel's trust store on
top of the system roots (for a corporate re-signing proxy or an on-prem CA).
Required rather than relying on SSL_CERT_FILE: the kernel's TLS stack (rustls)
does not read that environment variable.
- WithKernelSkipHostnameVerify() skips only the certificate hostname check while
keeping chain validation (finer-grained than WithSkipTLSHostVerify).

Setting either without WithUseKernel makes Connect fail with an error wrapping the
sentinel ErrRequiresKernelBackend (the mirror of ErrNotSupportedByKernel), so the
"add WithUseKernel or remove it" case is detectable with errors.Is rather than by
matching message text.

Features that live above the backend seam are inherited unchanged: the
database/sql connection pool (each connection wraps one kernel session),
per-connection telemetry (CREATE_SESSION, EXECUTE_STATEMENT, and DELETE_SESSION
Expand Down Expand Up @@ -332,6 +349,11 @@ on the error message text:
// retry with the default backend (omit WithUseKernel).
}

Conversely, a kernel-only option set without WithUseKernel is rejected on the
default (Thrift) path with an error wrapping the mirror sentinel
ErrRequiresKernelBackend, detectable the same way with
errors.Is(err, dbsqlerr.ErrRequiresKernelBackend).

Example usage:

import (
Expand Down
9 changes: 9 additions & 0 deletions errors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,15 @@ var ErrNotSupportedByKernel error = errors.New("not supported by the kernel back
// ErrNotSupportedByKernel — instead of matching on message text.
var ErrKernelNotCompiled error = errors.New("the SEA-via-kernel backend is not compiled into this binary")

// value to be used with errors.Is() to determine that a kernel-only option (e.g.
// WithKernelTrustedCerts / WithKernelSkipHostnameVerify) was set without
// WithUseKernel, so the default (Thrift) backend rejected it rather than
// connecting with a weaker-than-intended TLS trust store. This is the mirror of
// ErrNotSupportedByKernel — that sentinel means "the kernel can't honor this
// option"; this one means "this option requires the kernel". Lets a caller detect
// the case programmatically instead of matching on message text.
var ErrRequiresKernelBackend error = errors.New("requires the SEA-via-kernel backend")

// Base interface for driver errors
type DBError interface {
// Descriptive message describing the error
Expand Down
55 changes: 55 additions & 0 deletions internal/backend/kernel/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ type Config struct {
// so the kernel path relaxes both to match.
TLSSkipVerify bool

// Experimental kernel-only TLS knobs (from the WithKernel* options). These
// have no Thrift-path equivalent and are set via config.KernelExperimental.
// Empty/false fields are simply not applied. TLSTrustedCertsPEM is a custom CA
// bundle added on top of the system roots; TLSSkipHostnameVerify skips only the
// hostname check (finer-grained than the blanket TLSSkipVerify above).
TLSTrustedCertsPEM []byte
TLSSkipHostnameVerify bool

// ProxyURL configures an HTTP proxy, already resolved for this endpoint from
// the same HTTP(S)_PROXY / NO_PROXY environment the Thrift path uses (NO_PROXY
// is applied during resolution). Empty leaves the kernel on a direct
Expand Down Expand Up @@ -166,6 +174,14 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
}
}

// Experimental kernel-only TLS: a custom CA bundle and an independent hostname
// skip (finer-grained than the blanket InsecureSkipVerify above). The kernel's
// rustls stack ignores SSL_CERT_FILE, so a custom CA must be handed over
// explicitly.
if err := k.applyKernelTLS(cfg); err != nil {
return err
}

// Proxy: only when the environment configured one for this endpoint. NO_PROXY
// was already applied during resolution, so no bypass list is needed here;
// any credentials are carried in the URL userinfo (Go's proxy-env convention),
Expand Down Expand Up @@ -238,6 +254,31 @@ func (k *KernelBackend) OpenSession(ctx context.Context) error {
return nil
}

// applyKernelTLS forwards the experimental kernel-only TLS knobs to the session
// config: a custom CA bundle and an independent hostname-skip. Each is a no-op
// when its field is unset, so this is safe to call unconditionally. (mTLS client
// cert/key is a separate follow-up — it needs a kernel C-ABI setter that is not
// yet on kernel main.)
func (k *KernelBackend) applyKernelTLS(cfg *C.KernelSessionConfig) error {
if len(k.cfg.TLSTrustedCertsPEM) > 0 {
ca := newCBytes(k.cfg.TLSTrustedCertsPEM)
defer ca.free()
if err := call(func() C.KernelStatusCode {
return C.kernel_session_config_set_tls_trusted_certs(cfg, ca.ptr, ca.len)
}); err != nil {
return fmt.Errorf("kernel: set_tls_trusted_certs: %w", toConnError(err))
}
}
if k.cfg.TLSSkipHostnameVerify {
if err := call(func() C.KernelStatusCode {
return C.kernel_session_config_set_tls_skip_hostname_verification(cfg, C.bool(true))
}); err != nil {
return fmt.Errorf("kernel: set_tls_skip_hostname_verification: %w", toConnError(err))
}
}
return nil
}

// setAuth applies the resolved auth form to the session config via exactly one
// kernel_session_config_set_auth_* call. PAT and M2M are plain value setters; U2M
// records the client id / redirect port / scopes and the kernel owns the browser
Expand Down Expand Up @@ -301,6 +342,20 @@ func trySetAuth(auth Auth) error {
return k.setAuth(cfg)
}

// trySetKernelTLS allocates a throwaway session config, applies the experimental
// TLS knobs from cfg to it, and frees it — the analogous test seam to trySetAuth,
// so a tagged test can exercise the real byte-buffer cgo setter (trusted certs)
// and the hostname-skip setter end to end. Not used in production.
func trySetKernelTLS(cfg Config) error {
var c *C.KernelSessionConfig
if err := call(func() C.KernelStatusCode { return C.kernel_session_config_new(&c) }); err != nil {
return fmt.Errorf("config_new: %w", err)
}
defer C.kernel_session_config_free(c)
k := &KernelBackend{cfg: cfg}
return k.applyKernelTLS(c)
}

// applyInitialNamespace runs USE CATALOG / USE SCHEMA to select the configured
// initial namespace, since the kernel C ABI exposes no catalog/schema setter.
// Identifiers are backtick-quoted (quoteIdent) so arbitrary names are safe. A
Expand Down
24 changes: 24 additions & 0 deletions internal/backend/kernel/cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,27 @@ func (s cStr) free() {
C.free(unsafe.Pointer(s.c))
}
}

// cBytes wraps C.CBytes with a guaranteed free, for the byte-buffer setters (e.g.
// a PEM CA bundle) that take a (*C.uint8_t, C.size_t) pair. The kernel copies the
// bytes into owned Rust memory on receipt, so freeing right after the call is
// safe. An empty slice yields a NULL pointer + 0 length (the setters reject that,
// which is what we want — an empty buffer is never valid).
// Use: cb := newCBytes(b); defer cb.free(); ...C.fn(cb.ptr, cb.len)...
type cBytes struct {
ptr *C.uint8_t
len C.size_t
}

func newCBytes(b []byte) cBytes {
if len(b) == 0 {
return cBytes{}
}
return cBytes{ptr: (*C.uint8_t)(C.CBytes(b)), len: C.size_t(len(b))}
}

func (b cBytes) free() {
if b.ptr != nil {
C.free(unsafe.Pointer(b.ptr))
}
}
25 changes: 25 additions & 0 deletions internal/backend/kernel/kernel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,31 @@ func TestSetAuthByMode(t *testing.T) {
}
}

// TestSetKernelTLS exercises the real cgo setters for the experimental kernel-only
// TLS knobs (the byte-buffer trusted-CA bundle + the hostname-skip bool) via the
// trySetKernelTLS seam — proving the (*C.uint8_t, C.size_t) marshalling and the C
// signatures. A failure here means the field→setter wiring or a C signature
// drifted.
func TestSetKernelTLS(t *testing.T) {
ca := []byte("-----BEGIN CERTIFICATE-----\nca\n-----END CERTIFICATE-----\n")
cases := []struct {
name string
cfg Config
}{
{"trusted certs only", Config{TLSTrustedCertsPEM: ca}},
{"skip hostname only", Config{TLSSkipHostnameVerify: true}},
{"both together", Config{TLSTrustedCertsPEM: ca, TLSSkipHostnameVerify: true}},
{"none (no-op)", Config{}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if err := trySetKernelTLS(c.cfg); err != nil {
t.Errorf("applyKernelTLS(%s) = %v, want nil", c.name, err)
}
})
}
}

// The pure error-classifier tests (TestIsBadConnection, TestIsSessionFatal,
// TestToConnError, TestToStatementErrorNeverBadConn) live in the untagged
// errors_classify_test.go so they run under CGO_ENABLED=0. The tests below need a
Expand Down
43 changes: 43 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,48 @@ type Config struct {
ThriftTransport string
ThriftProtocolVersion cli_service.TProtocolVersion
ThriftDebugClientProtocol bool

// KernelExperimental carries experimental, kernel-backend-only options that
// have no equivalent on the default (Thrift) path — currently the richer TLS
// surface (a trusted-CA bundle and an independent hostname-skip) the kernel
// exposes over its C ABI. It lives here on Config, NOT on UserConfig, so it
// stays off the stable exported/DSN surface (the same treatment TLSConfig and
// ArrowConfig get). nil means no experimental option was set. The Thrift
// backend rejects a non-nil value loudly; the kernel backend forwards it to
// the kernel C ABI. Mirrors Node's non-exported InternalConnectionOptions /
// Python's underscore-prefixed kwargs.
KernelExperimental *KernelExperimentalConfig
}

// KernelExperimentalConfig holds the experimental, kernel-only connection knobs
// set via the WithKernel* options. Every field is either forwarded to the kernel
// C ABI (kernel backend) or rejected (Thrift backend) — never silently dropped;
// the exhaustiveness guard TestKernelExperimentalFieldsClassified asserts this so
// a newly-added field can't slip through unclassified.
type KernelExperimentalConfig struct {
// TLSTrustedCertsPEM is a PEM CA bundle added to the kernel's trust store on
// top of the system roots (maps to kernel_session_config_set_tls_trusted_certs).
// Needed because the kernel's rustls stack ignores SSL_CERT_FILE, so a custom
// CA must be handed to the kernel explicitly.
TLSTrustedCertsPEM []byte
// TLSSkipHostnameVerify skips only the certificate hostname check, independent
// of the blanket InsecureSkipVerify (which relaxes both chain and hostname).
// Maps to kernel_session_config_set_tls_skip_hostname_verification.
TLSSkipHostnameVerify bool
}

// DeepCopy returns a deep copy of the experimental config, or nil for a nil
// receiver. The byte slice is copied so a mutation of the copy can't reach back
// into the original (the connector may DeepCopy the whole Config per conn).
func (k *KernelExperimentalConfig) DeepCopy() *KernelExperimentalConfig {
if k == nil {
return nil
}
cp := &KernelExperimentalConfig{TLSSkipHostnameVerify: k.TLSSkipHostnameVerify}
if k.TLSTrustedCertsPEM != nil {
cp.TLSTrustedCertsPEM = append([]byte(nil), k.TLSTrustedCertsPEM...)
}
return cp
}

// MetricViewMetadataConfKey is the server session conf that enables metric-view
Expand Down Expand Up @@ -110,6 +152,7 @@ func (c *Config) DeepCopy() *Config {
ThriftTransport: c.ThriftTransport,
ThriftProtocolVersion: c.ThriftProtocolVersion,
ThriftDebugClientProtocol: c.ThriftDebugClientProtocol,
KernelExperimental: c.KernelExperimental.DeepCopy(),
}
}

Expand Down
12 changes: 12 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -769,11 +769,23 @@ func TestConfig_DeepCopy(t *testing.T) {
ThriftTransport: "http",
ThriftProtocolVersion: cli_service.TProtocolVersion_SPARK_CLI_SERVICE_PROTOCOL_V8,
ThriftDebugClientProtocol: false,
KernelExperimental: &KernelExperimentalConfig{
TLSTrustedCertsPEM: []byte("ca-bundle"),
TLSSkipHostnameVerify: true,
},
}

cfg_copy := cfg.DeepCopy()
if !reflect.DeepEqual(cfg, cfg_copy) {
t.Errorf("DeepCopy() = %v, want %v", cfg_copy, cfg)
}
// The experimental block must be deep-copied, not aliased.
if cfg_copy.KernelExperimental == cfg.KernelExperimental {
t.Error("DeepCopy aliased KernelExperimental pointer")
}
cfg_copy.KernelExperimental.TLSTrustedCertsPEM[0] = 'X'
if cfg.KernelExperimental.TLSTrustedCertsPEM[0] == 'X' {
t.Error("DeepCopy aliased the KernelExperimental CA byte slice")
}
})
}
8 changes: 8 additions & 0 deletions kernel_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ func newKernelBackend(_ context.Context, cfg *config.Config) (backend.Backend, e
if cfg.TLSConfig != nil && cfg.TLSConfig.InsecureSkipVerify {
kc.TLSSkipVerify = true
}
// Experimental kernel-only TLS knobs (WithKernelTrustedCerts /
// WithKernelSkipHostnameVerify), if any. These have no Thrift-path equivalent
// (the connector rejects them on that path) and are forwarded verbatim to the
// kernel C ABI in OpenSession.
if ke := cfg.KernelExperimental; ke != nil {
kc.TLSTrustedCertsPEM = ke.TLSTrustedCertsPEM
kc.TLSSkipHostnameVerify = ke.TLSSkipHostnameVerify
}
// Proxy: the Thrift path uses http.ProxyFromEnvironment; mirror it by reading
// the same HTTP(S)_PROXY / NO_PROXY environment for the kernel.
kc.ProxyURL = proxyForEndpoint(cfg)
Expand Down
Loading