Documentation
¶
Overview ¶
Package decryptprofile is the named SSL-decryption-profile engine: a PAN-OS-style "how to decrypt" object that policy rules reference by name to control HOW an inspected (SSLAction=Inspect) tunnel is decrypted — whether to inspect natively as HTTP/2, the upstream server-certificate-verification posture, the unsupported-TLS failure posture, the TLS version floor/cap, and the per-stream inactivity bound. It is the "how" half that complements the "what to match" half already in package main's policy rules + sslbypass.
This engine owns storage + validation ONLY. The resolvers that turn a matched rule's profile reference into a runtime decision (resolveStripALPN and friends) live in package main on the proxy hot path, mirroring how catgroup exposes the pure store while categoryGroupMatchesHost stays in main.
Concurrency: an RWMutex protects the store; reads (GetByName) take RLock, writes rebuild order under Lock. All validation is enforced in Add/Update AND ReplaceAll, because profiles are written by THREE paths — the admin API, config import, and CP→DP snapshot apply — and a handler-only check would be bypassed by the latter two.
Index ¶
- Constants
- Variables
- func SetCertMigrationSink(fn func(profileName string))
- func Validate(p *Profile) error
- type Profile
- type Snapshot
- type Store
- func (s *Store) Add(p Profile) (*Profile, error)
- func (s *Store) Delete(name string) error
- func (s *Store) DeleteByID(id string) (string, error)
- func (s *Store) FailOpenScope(name string) (id, gen string, ok bool)
- func (s *Store) FailOpenScopeByID(id string) (scope, gen string, resolved bool)
- func (s *Store) GetByID(id string) *Profile
- func (s *Store) GetByName(name string) *Profile
- func (s *Store) List() []Profile
- func (s *Store) Load(path string) error
- func (s *Store) MutateDurable(ifVersion *int64, fn func() error) error
- func (s *Store) Names() []string
- func (s *Store) Path() string
- func (s *Store) Rename(id, newName string) (oldName string, err error)
- func (s *Store) ReplaceAll(profiles []Profile)
- func (s *Store) Save()
- func (s *Store) SaveErr() error
- func (s *Store) SetPathForTest(path string)
- func (s *Store) SnapshotView() Snapshot
- func (s *Store) Update(p Profile) error
- func (s *Store) UpdateByID(id string, p Profile) error
- func (s *Store) Version() int64
- type VersionConflictError
Constants ¶
const ( MinStallSecs = 5 MaxStallSecs = 3600 )
Clamp bounds for StallTimeoutSecs. 0 means "engine default" (the caller substitutes sslInspectBodyStallTimeout); any other value must fall in [MinStallSecs, MaxStallSecs]. A too-low value kills legitimate long-poll/SSE/ large-download streams; a too-high value lets a slow-loris stream pin a handler + scan buffer. MaxStallSecs matches the default tunnel-idle ceiling (1h).
Variables ¶
var ErrNameTaken = errors.New("name already in use")
ErrNameTaken marks a name-collision refusal (create against an existing name, or rename onto a name a DIFFERENT profile owns). Handlers map it to a 409 — the server-authoritative refusal, checked under the store lock so a concurrent create/rename cannot slip past a handler-level pre-check.
var ErrPersist = errors.New("decryption profiles: durable persistence failed")
ErrPersist marks a durable-persistence failure surfaced by MutateDurable AFTER the in-memory mutation was rolled back: nothing durable changed and the in-memory store was restored to the pre-mutation truth. Handlers map it to a 5xx — never a success.
Functions ¶
func SetCertMigrationSink ¶ added in v1.0.134
func SetCertMigrationSink(fn func(profileName string))
SetCertMigrationSink publishes the migration-notice callback (see certMigrationSink). A nil fn clears it. Call once at startup, before the store is loaded or any CP→DP sync begins.
Types ¶
type Profile ¶
type Profile struct {
ID string `json:"id"`
Name string `json:"name"`
// InspectHTTP2: nil = inherit (strip → HTTP/1.1, today's default); true =
// native HTTP/2 inspection; false = force strip/HTTP-1.1.
InspectHTTP2 *bool `json:"inspectHttp2,omitempty"`
// CertVerification of the upstream (origin) cert on the inspect leg:
// "" inherit (rule's TLSSkipVerify) | "strict" (verify, block untrusted/expired)
// | "skip" (no verification).
//
// The retired "permissive" value is no longer accepted (its documented
// allow-on-failure semantics were never implemented; it verified like
// "strict"). A legacy persisted "permissive" is fail-closed-migrated to
// "strict" on load/sync — see migrateLegacyCertVerification.
CertVerification string `json:"certVerification,omitempty"`
// OnUnsupported posture when the origin TLS can't be inspected (version/cipher
// below floor): "" inherit | "fail-close" (drop — today's behavior) | "fail-open"
// (raw-relay bypass — DEFERRED, superseded by OnInspectError for fail-open).
OnUnsupported string `json:"onUnsupported,omitempty"`
// OnInspectError is the adaptive decryption-exclusion / fail-open posture when
// an inspected tunnel CANNOT be established because the host is incompatible
// with inspection (origin demands a client cert we can't present, or the TLS
// parameters are unsupported, or a pinned client rejects our forged leaf):
// "" inherit (fail-close, today's behavior) | "fail-close" (502/drop) |
// "fail-open" (record the host in the auto-exclusion cache after a
// confirm-count of distinct clients, rescue the current session where it has
// not yet committed to the client, and bypass subsequent sessions to the host
// until the entry expires). It deliberately does NOT fire on an untrusted/
// expired origin cert — that stays a block. See internal/autoexclude.
OnInspectError string `json:"onInspectError,omitempty"`
// MinTLSVersion / MaxTLSVersion floor and cap on the inspect handshakes:
// "" inherit | "1.2" | "1.3".
MinTLSVersion string `json:"minTlsVersion,omitempty"`
MaxTLSVersion string `json:"maxTlsVersion,omitempty"`
// StallTimeoutSecs per-stream inactivity bound; 0 = engine default, else
// clamped to [MinStallSecs, MaxStallSecs].
StallTimeoutSecs int `json:"stallTimeoutSecs,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
// contains filtered or unexported fields
}
Profile is a named decryption profile. Zero/empty fields mean "inherit the default" so an absent or partially-filled profile never silently changes today's behavior (back-compat with the pre-profile inline rule fields).
func (*Profile) SecurityGen ¶ added in v1.0.137
SecurityGen returns the profile's security generation (see computeSecurityGen). The store precomputes it on every write; for a Profile constructed OUTSIDE the store (tests, direct literals, a copyOut value) it computes on demand, so the value is always correct and identical to the stored one.
type Snapshot ¶ added in v1.0.218
Snapshot is one coherent read of the whole list contract: the profiles, the derived name list, and the durable fence version, all describing the SAME store state (POST-2D-A COHERENT-READ CORRECTION DISCOVERED DURING 2D-B REVIEW).
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store manages persistent decryption profiles with O(1) name lookups.
func (*Store) Add ¶
Add creates a new profile. Validates fields, then name uniqueness. Assigns a short unique ID (a truncated UUID) when absent (ID-preserving otherwise).
func (*Store) DeleteByID ¶ added in v1.0.87
DeleteByID removes the profile with the given stable ID. Returns the removed profile's name (for audit) or an error if not found.
func (*Store) FailOpenScope ¶ added in v1.0.82
FailOpenScope returns the profile's ID, its precomputed security generation, and true IFF a profile with the given name exists AND opts into fail-open (OnInspectError=="fail-open"). It is a HOT-PATH accessor (resolveSSLAction calls it per CONNECT for fail-open rules): it reads only three stored string fields under the RLock and returns NO copy, avoiding the copyOut allocation a full GetByName pays. The gen fences the learned-exclusion cache to the exact inspection posture, so a security-relevant profile edit invalidates entries without re-hashing here. The learn/cold paths keep the copy-returning accessors.
func (*Store) FailOpenScopeByID ¶ added in v1.0.95
FailOpenScopeByID resolves the autoexclude scope + security generation by the profile's stable ULID (references-by-id / rename-safe). resolved=true iff a profile with that id EXISTS; scope is its ID and gen its precomputed security generation when that profile is fail-open, else both "". The ID is AUTHORITATIVE: a resolved fail-close profile returns ("", true) so the caller does NOT fall back to the name — otherwise a rule whose id points at a fail-close profile but whose stale name points at a different fail-open one could get its (fail-close) session bypassed, violating the "fail-close is un-poisonable" invariant. Mirrors resolveDecryptionProfile: name fallback only when the id resolves to no profile at all. No-copy fast path; O(profiles), a small admin set, only on the SSL-inspect CONNECT path.
func (*Store) GetByID ¶ added in v1.0.87
GetByID returns a copy of the profile with the given stable ID, or nil.
func (*Store) GetByName ¶
GetByName returns a profile by name (case-insensitive). O(1). nil if not found.
func (*Store) Load ¶
Load reads profiles from a JSON file. Invalid profiles on disk are skipped with a log line (fail-safe — a corrupt entry never blocks startup or the valid ones).
func (*Store) MutateDurable ¶ added in v1.0.218
MutateDurable runs ONE admin mutation with the OPTIONAL expected-version fence AND the durable persist evaluated in the same serialized critical section — the exact contract documented on catgroup.Store.MutateDurable (fence mismatch ⇒ *VersionConflictError, fn error ⇒ atomic rollback, persist failure ⇒ rollback + ErrPersist, ErrReplacedNotSynced ⇒ landed-content success WITH the epoch, since content + version are one atomic envelope). ReplaceAll holds the same mutMu, so no writer can alter the fenced domain between the version comparison and the mutation.
func (*Store) Rename ¶ added in v1.0.95
Rename changes the display name of the profile with the given stable ID, re-keying the name index (references-by-id: rules link by ID, so the rename is safe — the caller cascades the denormalized name onto referencing rules). Validates the new name is non-empty and not already taken by a DIFFERENT profile. Returns the OLD name (for audit + the caller's cascade). A case-only change updates the display name in place without a collision check.
func (*Store) ReplaceAll ¶
ReplaceAll atomically replaces all profiles (cluster sync / rollback / import). Invalid profiles are skipped (fail-safe) so a bad remote/imported entry never takes down the store or the valid entries. IDs are PRESERVED as provided (backfilled only when empty) so a capture→apply→re-capture round-trip is stable. Bulk install = a content change: the client-visible fence advances so any admin edit loaded against the pre-install contents conflicts instead of silently overwriting the installed truth.
SERIALIZATION (2D-A fence correction, Blocker B): ReplaceAll holds the SAME mutMu as MutateDurable, so a bulk install can never interleave between a client's ifVersion comparison and its protected mutation — the two writer classes observe exactly one serial order, and the fence generation cannot alias across them.
func (*Store) Save ¶
func (s *Store) Save()
Save persists the current profiles to disk (atomic write). No-op when path unset. Best-effort legacy wrapper for old non-critical callers; the hardened v2 mutation path (MutateDurable) uses the error-returning SaveErr and rolls back on failure.
func (*Store) SaveErr ¶ added in v1.0.218
SaveErr is the error-returning persistence core (2D-A durable-or-nothing). Content and the fence epoch are ONE envelope in ONE atomic write, so they can never diverge durably — including under ErrReplacedNotSynced, where the landed replacement carries the new epoch with the new content. The retired legacy sidecar is removed once the envelope has landed.
SaveErr is the PUBLIC entry: it acquires mutMu FIRST (2D-A commit-boundary correction), so a standalone save orders against the whole mutation domain and can never observe — let alone publish — the memory state of an in-flight MutateDurable transaction: fn's uncommitted content paired with the not-yet-advanced epoch. Without this, a caller-side Save (the production ReplaceAll+Save bulk shape) racing an admin mutation could persist uncommitted-new-content + old-epoch; if that mutation then failed its own publication and rolled back, the failed, unacknowledged mutation stayed on disk. MutateDurable already holds mutMu and calls saveErrLocked directly — mutMu is not reentrant, so an internal SaveErr call from inside the mutation path would deadlock and must never be added.
func (*Store) SetPathForTest ¶
SetPathForTest points persistence at path without loading.
func (*Store) SnapshotView ¶ added in v1.0.218
SnapshotView captures profiles + names + version under ONE hold of the read lock. List()/Names()/Version() assembled by a caller are three independent reads — a writer landing between any two hands the client rows from one state paired with the fence version of another, and an edit from that pair passes the ifVersion fence against content the client never saw. Handlers serving the fenced list contract must use this, never the trio.
COMMITTED-SNAPSHOT rule (transactional-read correction): SnapshotView also acquires mutMu FIRST. MutateDurable's fn mutates the store under mu and RELEASES mu before the version bump and the durable publication, and a persist failure rolls everything back to the pre-mutation state at the SAME version — so a snapshot taken inside that window returns mutated rows paired with the UNBUMPED version, and after the rollback a client edit derived from those phantom rows PASSES the ifVersion fence against the restored tree. Holding mutMu makes the snapshot wait until the open transaction reaches success or rollback; it then describes committed truth only. Cold admin read — hot-path accessors never touch mutMu. Lock order: mutMu → mu (the store's established order).
func (*Store) Update ¶
Update replaces an existing profile's fields (by name). Preserves ID + CreatedAt.
func (*Store) UpdateByID ¶ added in v1.0.87
UpdateByID replaces the content of the profile with the given stable ID (rename-safe addressing). Like the name-keyed Update, it edits content and keeps the profile's current name + CreatedAt + ID — position/identity are not changed by an edit. Returns error if no profile carries the id.
type VersionConflictError ¶ added in v1.0.218
type VersionConflictError struct {
Current int64 // the store generation at the locked moment of the check
Asserted int64 // the caller's ?ifVersion= assertion
}
VersionConflictError is the optimistic-concurrency failure returned by MutateDurable when the caller's asserted store generation no longer matches: another admin mutated the store since the caller loaded it. The mutation never ran. Mirrors package main's policyVersionConflictError contract.
func (*VersionConflictError) Error ¶ added in v1.0.218
func (e *VersionConflictError) Error() string