Documentation
¶
Index ¶
- Constants
- func ComputeHPowerTable(h [16]byte) [11][16]byte
- func GF128HPower(h [16]byte, power uint32) [16]byte
- func GF128Mul(a, b [16]byte) [16]byte
- func GHASHCompute(h [16]byte, blocks [][16]byte) [16]byte
- func GHASHIncrementalDelta(h [16]byte, changedBlocks map[int][16]byte, totalCTBlocks int) [16]byte
- type BoringSSLOffsets
- type BunOffsets
- type GoTLSOffsets
- type TLSOffsets
- type TLSUprobeManager
- func (m *TLSUprobeManager) AttachTLS(pid int, cgroupID uint64) error
- func (m *TLSUprobeManager) Close() error
- func (m *TLSUprobeManager) DumpDebugCounters()
- func (m *TLSUprobeManager) PollEvents(ctx context.Context) error
- func (m *TLSUprobeManager) PollExecEvents(ctx context.Context) error
- func (m *TLSUprobeManager) PopulateTrustedDNSServers(ips []net.IP) error
- func (m *TLSUprobeManager) RecordCgroupNetns(cgroupID uint64, pid int)
- func (m *TLSUprobeManager) TrackCgroup(cgroupID uint64, cgroupPath string) error
- func (m *TLSUprobeManager) TrackTGID(tgid uint32) error
- func (m *TLSUprobeManager) UntrackCgroup(cgroupID uint64) error
- func (m *TLSUprobeManager) UntrackTGID(tgid uint32) error
Constants ¶
const AESKeyRoundsOff = 240
AESKeyRoundsOff is the offset of AES_KEY.rounds past rd_key (rd_key is uint32_t[60] = 240 bytes). Constant across BoringSSL builds.
Variables ¶
This section is empty.
Functions ¶
func ComputeHPowerTable ¶
ComputeHPowerTable precomputes H^(2^i) for i = 0..10, giving powers H^1, H^2, H^4, H^8, ..., H^1024. This table enables the eBPF kprobe to compute arbitrary H^n via at most 11 GF(2^128) multiplications (one per set bit in n).
func GF128HPower ¶
GF128HPower computes H^power in GF(2^128) via square-and-multiply.
func GF128Mul ¶
GF128Mul multiplies two 128-bit elements in GF(2^128) using the GHASH bit convention. Both inputs and the result are 16-byte arrays in big-endian byte order with MSB-first bit numbering.
func GHASHCompute ¶
GHASHCompute computes GHASH(H, data) where data is the concatenation of all GHASH input blocks (AAD padded + ciphertext padded + length block). Returns the 16-byte GHASH output.
func GHASHIncrementalDelta ¶
GHASHIncrementalDelta computes the tag delta when ciphertext blocks change. changedBlocks maps block index (0-based within ciphertext) to (old XOR new) delta. totalCTBlocks is the total number of ciphertext blocks. Returns the 16-byte tag delta: new_tag = old_tag XOR delta.
Types ¶
type BoringSSLOffsets ¶ added in v0.2.0
type BoringSSLOffsets struct {
SSLToS3 uint32 // SSL* → SSL3_STATE* (pointer deref)
S3ToAEAD uint32 // SSL3_STATE* → SSLAEADContext* (pointer deref, aead_write_ctx)
AEADToAESKey uint32 // SSLAEADContext* → AES_KEY.rd_key (direct read)
SSLToWBIO uint32 // SSL* → wbio BIO* (socket fd recovery, same role as OpenSSL)
}
BoringSSLOffsets contains the struct offsets for walking an SSL* object to the AES-GCM hash key on a BoringSSL build. Unlike OpenSSL — whose provider GCM context persists the raw 16-byte hash subkey H (gcm128_context.H) — BoringSSL's GCM128_KEY stores only the precomputed, platform-specific GHASH powers table (Htable); the raw subkey ghash_key = AES_encrypt(0) is a local in CRYPTO_gcm128_init_aes_key that is discarded after the table is built.
The chain is shorter than OpenSSL's (no OSSL_RECORD_LAYER indirection):
SSL* + SSLToS3 → SSL3_STATE* (pointer deref)
+ S3ToAEAD → SSLAEADContext* (pointer deref; UniquePtr aead_write_ctx)
+ AEADToAESKey → AES_KEY.rd_key (direct read of the round-key schedule)
AEADToAESKey collapses the embedded hops SSLAEADContext.ctx_ (EVP_AEAD_CTX, an embedded ScopedEVP_AEAD_CTX) → EVP_AEAD_CTX.state (embedded union) → aead_aes_gcm_ctx.key (GCM128_KEY) → gcm128_key_st.aes (AES_KEY). All hops after the first two are embedded structs, so AEADToAESKey is one additive offset.
Why the AES_KEY and not the GHASH table: BoringSSL's GCM128_KEY persists only the precomputed, CPU-specific GHASH powers table (Htable) — never the raw 16-byte subkey H that kloak's tag recomputation needs. But it DOES keep the AES round-key schedule, and BoringSSL itself derives H exactly as H = AES_encrypt(0) over that schedule (see CRYPTO_gcm128_init_aes_key). So the BPF data plane reads AES_KEY.rd_key + rounds and recomputes H = AES_block(0) in-kernel (see the BoringSSL branch in pkg/ebpf/bpf/tls_uprobe.c). This is architecture- and GHASH-impl-independent, unlike recovering H from the Htable representation.
AES_KEY layout: uint32_t rd_key[60] then uint32_t rounds, so the round count sits at AESKeyRoundsOff (240) bytes past rd_key.
Offsets are discovered via pahole in tools/boringssl-offsets/ and committed to tools/boringssl-offsets/results/; TestBoringSSLOffsets_AgainstReferenceJSON asserts this table matches them.
func DetectBoringSSL ¶ added in v0.2.0
func DetectBoringSSL(pid int, libPath string) (key string, offsets BoringSSLOffsets, err error)
DetectBoringSSL reports whether the library/binary at libPath is BoringSSL and, if so, returns its struct offsets. The library is accessed via /proc/<pid>/root/<libPath> to reach into the container's overlay filesystem (libPath may instead be an absolute /proc path such as /proc/<pid>/exe).
BoringSSL does not embed a resolvable version, so all builds map to the "default" offset row; the returned key is "default".
type BunOffsets ¶ added in v0.2.0
type BunOffsets struct {
SSLWriteOffset uint64
BoringSSL BoringSSLOffsets
}
BunOffsets holds the file offset of SSL_write in a Bun single-executable plus the BoringSSL struct chain needed to walk SSL* → AES-GCM H subkey.
Bun statically links BoringSSL with all internal symbols stripped from production and profile builds. The SSL_write file offset is pre-extracted from the official Bun profile release (same optimisation flags as production, symbol table retained) for each (version, arch) pair and stored here. Offset extraction is automated by tools/bun-offsets/.
BoringSSL struct offsets reuse the same SSL→s3→aead_write_ctx→AES_KEY chain as the shared-library BoringSSL path (see boringssl_offsets.go). The layout has been stable across tracked Bun+BoringSSL revisions.
func DetectBun ¶ added in v0.2.0
func DetectBun(binPath string) (BunOffsets, string, bool)
DetectBun scans the binary at binPath for an embedded Bun version string ("bun/X.Y.Z"). Returns the pre-computed BunOffsets for the detected (version, arch) pair, the version string, and true on success.
Only the first 64 MiB are scanned: Bun single-executables append the JS bundle after the ELF binary, and scanning the full file is unnecessary and slow. Scanning is done in 1 MiB chunks to avoid a single large heap allocation; the same file descriptor is reused for the ELF arch check.
type GoTLSOffsets ¶
type GoTLSOffsets struct {
ConnToCipher uint32 // Conn.out offset + halfConn.cipher offset + 8 (interface data_ptr)
AEADIfaceOff uint32 // prefixNonceAEAD.aead offset + 8 (interface data_ptr)
H2HiOff uint32 // GCM + off → high 64 bits of H×2 (byte-swapped)
H2LoOff uint32 // GCM + off → low 64 bits of H×2 (byte-swapped)
ConnVersOff uint32 // Conn + off → uint16 negotiated TLS version (e.g. 0x0303, 0x0304)
}
GoTLSOffsets contains struct offsets for extracting the GHASH H key from Go's crypto/tls internal structures. The layout must match struct go_tls_offsets in tls_uprobe.c exactly.
H extraction chain (3 pointer dereferences + GF(2^128) halving):
Conn + ConnToCipher → cipher interface data_ptr → AEAD wrapper
+ AEADIfaceOff → inner aead interface data_ptr → GCM struct
+ H2HiOff / H2LoOff → H×2 (two 64-bit words from productTable[224])
Go's gcmAesInit assembly computes H = AES_K(0^128), byte-swaps, then doubles in GF(2^128) before storing at productTable offset 224. The BPF code reads the two words and applies GF halving to recover standard H.
The word order at offset 224 is architecture-dependent:
- AMD64 (PSHUFB full reversal): hi word at +8, lo word at +0
- ARM64 (VREV64 per-lane reversal): hi word at +0, lo word at +8
func DetectGoTLSOffsets ¶
func DetectGoTLSOffsets(exePath string) (version string, offsets GoTLSOffsets, err error)
DetectGoTLSOffsets reads a Go binary and returns the struct offsets needed for H extraction. Tries DWARF first, then falls back to version-based lookup.
type TLSOffsets ¶
type TLSOffsets struct {
SSLToWRL uint32 // 4-hop: SSL* → WRL*. 3-hop: SSL* → EVP_CIPHER_CTX* (when WRLToEncCtx=0)
WRLToEncCtx uint32 // wrl* → EVP_CIPHER_CTX*. 0 = 3-hop mode (no record layer)
EncCtxToAlgctx uint32 // enc_ctx* + off → algctx/PROV_GCM_CTX* (pointer deref)
AlgctxToH uint32 // algctx* + off → H (16 bytes, direct read)
SSLToVersion uint32 // SSL_CONNECTION* + off → int version (0xFFFFFFFF=unknown, use heuristic)
// SSLToWBIO is the offset from the SSL/SSL_CONNECTION pointer to its
// `wbio` BIO* field. The BPF data plane reads SSL → wbio → BIO.num to
// recover the socket fd, then looks it up in conn_ip_map to confirm
// client-mode (set by connect()). 3.0/3.1 use struct ssl_st where
// wbio sits at 24; 3.2+ wrapped SSL in ssl_connection_st and moved it
// to 88. Before this field existed, the BPF program hardcoded 88 —
// which silently broke every 3.0/3.1 workload by reading garbage and
// tripping the server-mode guard in bpf_uprobe_ssl_write.
SSLToWBIO uint32
}
TLSOffsets contains struct offsets for extracting TLS key material from an SSL* object at runtime. Two chain variants exist:
4-hop (OpenSSL 3.2+):
SSL* + SSLToWRL → WRL* + WRLToEncCtx → EVP_CIPHER_CTX* + EncCtxToAlgctx → algctx* + AlgctxToH → H
3-hop (OpenSSL 3.0-3.1):
SSL* + SSLToWRL → EVP_CIPHER_CTX* + EncCtxToAlgctx → algctx* + AlgctxToH → H
The 3-hop chain skips the record layer indirection. SSLToWRL stores the enc_write_ctx offset directly, and WRLToEncCtx=0 signals 3-hop mode to BPF.
These are version-specific and must match the OpenSSL build in the container.
func DetectOpenSSLVersion ¶
func DetectOpenSSLVersion(pid int, libPath string) (version string, offsets TLSOffsets, err error)
DetectOpenSSLVersion reads an OpenSSL/libssl shared library from a container's filesystem and determines its version. Returns the version string (e.g., "3.2.1") and the corresponding struct offsets.
The library is accessed via /proc/<pid>/root/<libPath> to reach into the container's overlay filesystem.
type TLSUprobeManager ¶
type TLSUprobeManager struct {
// contains filtered or unexported fields
}
TLSUprobeManager manages the loading and attaching of eBPF uprobes for TLS interception.
func NewTLSUprobeManager ¶
func NewTLSUprobeManager(secretSource secrets.Source, cgroupRoot, egressInterface string, log *zap.SugaredLogger) (*TLSUprobeManager, error)
NewTLSUprobeManager initializes a new uprobe manager.
egressInterface selects which interface(s) inside each target netns get the tc-egress patch program attached. Pass "auto" (or empty) to detect from the default route, an explicit name (e.g., "eth0", "wlp3s0") to pin, or "none"/"lo-only" to attach only to loopback. See the field comment on TLSUprobeManager.egressInterface for the full semantics.
func (*TLSUprobeManager) AttachTLS ¶
func (m *TLSUprobeManager) AttachTLS(pid int, cgroupID uint64) error
AttachTLS attaches system-wide eBPF uprobes to all TLS libraries in a container. The uprobes fire for ALL processes in the container (same overlay mount = same inode). The BPF cgroup filter restricts interception to tracked containers only.
func (*TLSUprobeManager) Close ¶
func (m *TLSUprobeManager) Close() error
Close releases all links and the eBPF manager.
Each link.Close on a uprobe is a kernel syscall that takes ~50-100 ms, and m.links accumulates one entry per uprobe attachment over the daemonset's lifetime. On a long-running cluster with churning pods this reaches into the thousands; closing serially blows past kubelet's terminationGracePeriodSeconds and the pod is SIGKILL'd before the Go runtime gets to flush exit hooks (which lost us the e2e covcounters flush, among other things). Close concurrently with a bounded worker pool so wall time stays inside the grace period regardless of how long the controller has been running.
func (*TLSUprobeManager) DumpDebugCounters ¶
func (m *TLSUprobeManager) DumpDebugCounters()
DumpDebugCounters reads and logs all debug counters from the BPF map.
func (*TLSUprobeManager) PollEvents ¶
func (m *TLSUprobeManager) PollEvents(ctx context.Context) error
PollEvents reads TLS events from the ring buffer and periodically syncs secrets to the eBPF map.
func (*TLSUprobeManager) PollExecEvents ¶
func (m *TLSUprobeManager) PollExecEvents(ctx context.Context) error
PollExecEvents reads process exec events from the proc_events ring buffer and attaches TLS uprobes to newly exec'd processes in tracked containers.
func (*TLSUprobeManager) PopulateTrustedDNSServers ¶
func (m *TLSUprobeManager) PopulateTrustedDNSServers(ips []net.IP) error
PopulateTrustedDNSServers adds trusted DNS server IPs to the BPF map and enables the whitelist. If ips is empty, the whitelist is not enabled and all DNS responses are accepted.
func (*TLSUprobeManager) RecordCgroupNetns ¶
func (m *TLSUprobeManager) RecordCgroupNetns(cgroupID uint64, pid int)
RecordCgroupNetns records the cgroup → netns inode mapping for a container process. This enables cleanup of the tcAttached entry (which pins the netns fd) when the cgroup is untracked on pod deletion.
func (*TLSUprobeManager) TrackCgroup ¶
func (m *TLSUprobeManager) TrackCgroup(cgroupID uint64, cgroupPath string) error
TrackCgroup adds a container cgroup ID to the tracked_cgroups map, enabling exec/exit tracepoint processing for processes in that cgroup.
func (*TLSUprobeManager) TrackTGID ¶
func (m *TLSUprobeManager) TrackTGID(tgid uint32) error
TrackTGID adds a process TGID to the tracked_tgids map, enabling DNS/connect tracepoint processing for that process.
func (*TLSUprobeManager) UntrackCgroup ¶
func (m *TLSUprobeManager) UntrackCgroup(cgroupID uint64) error
UntrackCgroup removes a container cgroup ID from the tracked_cgroups map and cleans up the associated tc attachment state. When the last cgroup referencing a netns is untracked, the pinned netns fd is closed, allowing the kernel to free the inode for reuse.
func (*TLSUprobeManager) UntrackTGID ¶
func (m *TLSUprobeManager) UntrackTGID(tgid uint32) error
UntrackTGID removes a process TGID from the tracked_tgids map. go_tls_offset_config is keyed by cgroup_id, not tgid, and is cleaned up via UntrackCgroup when the container is removed.