diff --git a/connector.go b/connector.go index 1dd89264..3838cb9d 100644 --- a/connector.go +++ b/connector.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "database/sql/driver" + "fmt" "net/http" "net/url" "regexp" @@ -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" @@ -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 { @@ -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 { + return func(c *config.Config) { + kernelExperimental(c).TLSSkipHostnameVerify = true + } +} diff --git a/doc.go b/doc.go index 1f3c7e92..a3511c3b 100644 --- a/doc.go +++ b/doc.go @@ -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 @@ -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 ( diff --git a/errors/errors.go b/errors/errors.go index 88bce5e2..8eac7877 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -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 diff --git a/internal/backend/kernel/backend.go b/internal/backend/kernel/backend.go index 7c74e19e..e6540081 100644 --- a/internal/backend/kernel/backend.go +++ b/internal/backend/kernel/backend.go @@ -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 @@ -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), @@ -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 @@ -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 diff --git a/internal/backend/kernel/cgo.go b/internal/backend/kernel/cgo.go index eaab36f2..7aabd2cf 100644 --- a/internal/backend/kernel/cgo.go +++ b/internal/backend/kernel/cgo.go @@ -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)) + } +} diff --git a/internal/backend/kernel/kernel_test.go b/internal/backend/kernel/kernel_test.go index e3e62f54..8582a501 100644 --- a/internal/backend/kernel/kernel_test.go +++ b/internal/backend/kernel/kernel_test.go @@ -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 diff --git a/internal/config/config.go b/internal/config/config.go index 58c92123..2113a64f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 @@ -110,6 +152,7 @@ func (c *Config) DeepCopy() *Config { ThriftTransport: c.ThriftTransport, ThriftProtocolVersion: c.ThriftProtocolVersion, ThriftDebugClientProtocol: c.ThriftDebugClientProtocol, + KernelExperimental: c.KernelExperimental.DeepCopy(), } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a4b9b54f..c4160c25 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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") + } }) } diff --git a/kernel_backend.go b/kernel_backend.go index 9c8092ce..470e2424 100644 --- a/kernel_backend.go +++ b/kernel_backend.go @@ -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) diff --git a/kernel_experimental_test.go b/kernel_experimental_test.go new file mode 100644 index 00000000..1c3534d2 --- /dev/null +++ b/kernel_experimental_test.go @@ -0,0 +1,160 @@ +package dbsql + +import ( + "context" + "errors" + "reflect" + "testing" + + "github.com/databricks/databricks-sql-go/internal/config" + + dbsqlerr "github.com/databricks/databricks-sql-go/errors" +) + +// This file is untagged (no cgo) so its tests — the experimental-option +// classification guard, the option→config wiring, and the Thrift fail-loud +// signal — run in the default CGO_ENABLED=0 build alongside the other +// kernel_config tests. + +// kernelExperimentalFieldDisposition records how each experimental (kernel-only) +// option is handled. Every experimental field is forwarded to the kernel C ABI +// (there is no "inert" experimental knob — they exist precisely because the kernel +// supports them) and rejected on the Thrift path (the connector fails loud when +// KernelExperimental is non-nil). A new field on config.KernelExperimentalConfig +// without an entry here fails TestKernelExperimentalFieldsClassified, forcing a +// deliberate decision and a setter in KernelBackend.OpenSession so it can't be +// silently dropped. +var kernelExperimentalFieldDisposition = map[string]string{ + "TLSTrustedCertsPEM": "forwarded", // set_tls_trusted_certs + "TLSSkipHostnameVerify": "forwarded", // set_tls_skip_hostname_verification +} + +func TestKernelExperimentalFieldsClassified(t *testing.T) { + tp := reflect.TypeOf(config.KernelExperimentalConfig{}) + classified := make(map[string]bool, tp.NumField()) + for i := 0; i < tp.NumField(); i++ { + name := tp.Field(i).Name + classified[name] = true + if _, ok := kernelExperimentalFieldDisposition[name]; !ok { + t.Errorf("config.KernelExperimentalConfig field %q is not classified. Add it to "+ + "kernelExperimentalFieldDisposition and wire a setter in KernelBackend.OpenSession / "+ + "newKernelBackend so it isn't silently dropped on the kernel path.", name) + } + } + for name := range kernelExperimentalFieldDisposition { + if !classified[name] { + t.Errorf("kernelExperimentalFieldDisposition has %q but config.KernelExperimentalConfig no longer does; remove it", name) + } + } +} + +// The experimental WithKernel* options are rejected on the default (Thrift) path +// so a caller who forgets WithUseKernel learns the option had no effect. The +// option builders set config.KernelExperimental; the connector's backend-selection +// branch is what rejects it. We assert the option→config wiring here (a non-nil +// KernelExperimental after applying a WithKernel* option is the signal the Thrift +// branch keys off). +func TestWithKernelTLSOptionsSetExperimental(t *testing.T) { + cases := []struct { + name string + opt ConnOption + verify func(*config.KernelExperimentalConfig) bool + }{ + {"trusted certs", WithKernelTrustedCerts([]byte("ca")), func(k *config.KernelExperimentalConfig) bool { + return string(k.TLSTrustedCertsPEM) == "ca" + }}, + {"skip hostname", WithKernelSkipHostnameVerify(), func(k *config.KernelExperimentalConfig) bool { + return k.TLSSkipHostnameVerify + }}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + cfg := config.WithDefaults() + c.opt(cfg) + if cfg.KernelExperimental == nil { + t.Fatalf("%s: KernelExperimental should be non-nil after the option", c.name) + } + if !c.verify(cfg.KernelExperimental) { + t.Errorf("%s: option did not set the expected field(s): %+v", c.name, cfg.KernelExperimental) + } + }) + } +} + +// End-to-end: setting a WithKernel* option WITHOUT WithUseKernel must make +// Connect fail loud on the default (Thrift) path rather than silently connect +// with a weaker-than-intended trust store. This exercises the connector's +// reject branch (not just option→config wiring) and asserts the error wraps the +// ErrRequiresKernelBackend sentinel so callers can detect it with errors.Is — +// the mirror of TestKernelBackendNotCompiledIn. Runs in the default +// CGO_ENABLED=0 build (no kernel linked in). +func TestWithKernelOptionsRejectedOnThriftPath(t *testing.T) { + cases := []struct { + name string + opt ConnOption + }{ + {"trusted certs", WithKernelTrustedCerts([]byte("ca"))}, + {"skip hostname", WithKernelSkipHostnameVerify()}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c, err := NewConnector( + WithServerHostname("example.cloud.databricks.com"), + WithPort(443), + WithHTTPPath("/sql/1.0/endpoints/12346a5b5b0e123a"), + WithAccessToken("supersecret"), + tc.opt, + ) + if err != nil { + t.Fatalf("NewConnector: %v", err) + } + // No WithUseKernel — the Thrift path must reject the kernel-only option. + if _, err = c.Connect(context.Background()); err == nil { + t.Fatal("Connect should reject a WithKernel* option on the Thrift path, got nil") + } else if !errors.Is(err, dbsqlerr.ErrRequiresKernelBackend) { + t.Errorf("error should wrap ErrRequiresKernelBackend; got: %v", err) + } + }) + } +} + +// WithKernelTrustedCerts must copy the PEM defensively, so a caller mutating its +// slice after applying the option (but before Connect) can't change the stored +// trust store. This is the option-set counterpart to TestKernelExperimentalDeepCopy +// (which covers the per-conn DeepCopy path). +func TestWithKernelTrustedCertsCopiesPEM(t *testing.T) { + pem := []byte("ca-bundle") + cfg := config.WithDefaults() + WithKernelTrustedCerts(pem)(cfg) + + // Mutate the caller's slice after the option ran. + pem[0] = 'X' + + if cfg.KernelExperimental == nil { + t.Fatal("KernelExperimental should be non-nil after WithKernelTrustedCerts") + } + if got := string(cfg.KernelExperimental.TLSTrustedCertsPEM); got != "ca-bundle" { + t.Errorf("WithKernelTrustedCerts aliased the caller's slice; stored %q, want %q", got, "ca-bundle") + } +} + +// DeepCopy must copy the CA byte slice, not alias it — the connector may DeepCopy +// the whole Config per conn, and a shared backing array would let one conn's +// mutation reach another. +func TestKernelExperimentalDeepCopy(t *testing.T) { + orig := &config.KernelExperimentalConfig{ + TLSTrustedCertsPEM: []byte("ca-bundle"), + TLSSkipHostnameVerify: true, + } + cp := orig.DeepCopy() + if cp == nil || string(cp.TLSTrustedCertsPEM) != "ca-bundle" || !cp.TLSSkipHostnameVerify { + t.Fatalf("DeepCopy lost data: %+v", cp) + } + cp.TLSTrustedCertsPEM[0] = 'X' + if orig.TLSTrustedCertsPEM[0] == 'X' { + t.Error("DeepCopy aliased the CA byte slice; a copy mutation reached the original") + } + if (*config.KernelExperimentalConfig)(nil).DeepCopy() != nil { + t.Error("nil.DeepCopy() should be nil") + } +}