ebpf

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 8, 2026 License: AGPL-3.0 Imports: 32 Imported by: 0

Documentation

Index

Constants

View Source
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

func ComputeHPowerTable(h [16]byte) [11][16]byte

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

func GF128HPower(h [16]byte, power uint32) [16]byte

GF128HPower computes H^power in GF(2^128) via square-and-multiply.

func GF128Mul

func GF128Mul(a, b [16]byte) [16]byte

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

func GHASHCompute(h [16]byte, blocks [][16]byte) [16]byte

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

func GHASHIncrementalDelta(h [16]byte, changedBlocks map[int][16]byte, totalCTBlocks int) [16]byte

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 SecretBindingMode added in v0.2.0

type SecretBindingMode string

SecretBindingMode selects what the data plane does when a process redeems a placeholder of a secret its pod does not reference (see secret_acl in tls_uprobe.c).

const (
	// SecretBindingEnforce refuses the rewrite: the placeholder goes out
	// unchanged. This is the default and the BPF map's zero value.
	SecretBindingEnforce SecretBindingMode = "enforce"
	// SecretBindingAudit lets the rewrite go ahead but still reports it.
	// Meant for finding workloads that redeem secrets they don't mount
	// before switching to enforce.
	SecretBindingAudit SecretBindingMode = "audit"
)

func ParseSecretBindingMode added in v0.2.0

func ParseSecretBindingMode(s string) (SecretBindingMode, error)

ParseSecretBindingMode validates a --secret-binding value.

type TCAttachMode added in v0.2.0

type TCAttachMode string

TCAttachMode selects how the tc patch program is attached to an interface.

const (
	// TCAttachAuto uses TCX when the kernel supports it (Linux 6.6+) and
	// falls back to a classic clsact/cls_bpf filter otherwise.
	TCAttachAuto TCAttachMode = "auto"
	// TCAttachTCX always uses TCX links. Fails on kernels older than 6.6.
	TCAttachTCX TCAttachMode = "tcx"
	// TCAttachClsact always uses a classic clsact qdisc + cls_bpf filter.
	// Works on every kernel Kloak supports (5.17+). Mainly useful to
	// exercise the fallback path on a TCX-capable kernel.
	TCAttachClsact TCAttachMode = "clsact"
)

func ParseTCAttachMode added in v0.2.0

func ParseTCAttachMode(s string) (TCAttachMode, error)

ParseTCAttachMode validates a --tc-attach-mode value.

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

	// The next three drive ossl_read_gcm_h in the BPF data plane (issue #275).
	// 0 means "not calibrated": the data plane falls back to the legacy
	// "raw H non-zero ⇒ GCM" heuristic and has no AVX-512 recovery.
	//
	// AlgctxToGCMKey is algctx → GCM128_CONTEXT.key and AlgctxToKeySched is
	// algctx → PROV_AES_GCM_CTX.ks. Every keyed OpenSSL GCM context points
	// gcm.key at its own ks, so the pair is a self-pointer gate that non-GCM
	// contexts (whose bytes at AlgctxToH are unrelated) cannot pass.
	AlgctxToGCMKey   uint32
	AlgctxToKeySched uint32
	// AlgctxToVAESHKey1 is algctx → GCM128_CONTEXT.Htable[15]. On x86-64 CPUs
	// with AVX-512 + VAES + VPCLMULQDQ, OpenSSL ≥ 3.1 never writes the raw H
	// field; ossl_aes_gcm_init_avx512 stores HashKey_1 = bswap128(H)<<1 mod P
	// there instead, which the data plane inverts. 0 for versions without that
	// code path (3.0) or whose asm layout discovery did not recognise.
	AlgctxToVAESHKey1 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) BindPodSecrets added in v0.2.0

func (m *TLSUprobeManager) BindPodSecrets(podUID, pod string, cgroupIDs []uint64, refs []secrets.Ref)

BindPodSecrets records which secrets a pod may redeem and grants them to its container cgroups at once, so the pod's first TLS writes are rewritten. It replaces any previous binding for the pod: cgroups or references that went away are revoked at the next sync. podUID keys the binding; pod (namespace/name) is only used in logs.

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 periodically syncs secrets and pod bindings to the eBPF maps, and logs secret-binding violations reported by the data plane.

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) SetSecretBindingMode added in v0.2.0

func (m *TLSUprobeManager) SetSecretBindingMode(mode SecretBindingMode)

SetSecretBindingMode configures what the data plane does when a process redeems a placeholder of a secret its pod does not reference. The BPF default (an unwritten map) is enforce, so a failed write fails closed.

func (*TLSUprobeManager) SetTCAttachMode added in v0.2.0

func (m *TLSUprobeManager) SetTCAttachMode(mode TCAttachMode)

SetTCAttachMode selects how the tc patch program is attached (see TCAttachMode). Call it before any pod is tracked; the default is TCAttachAuto.

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) UnbindPod added in v0.2.0

func (m *TLSUprobeManager) UnbindPod(podUID string)

UnbindPod revokes everything a pod could redeem, immediately.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL