decryptprofile

package
v1.0.219 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: MIT Imports: 14 Imported by: 0

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

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

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

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

func Validate

func Validate(p *Profile) error

Validate checks a profile's fields. Enforced on EVERY write path (Add/Update/ ReplaceAll) so config-import and CP→DP snapshot-apply cannot smuggle an invalid profile onto the data plane. Does not check name uniqueness (that's store-scoped).

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

func (p *Profile) SecurityGen() string

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

type Snapshot struct {
	Profiles []Profile
	Names    []string
	Version  int64
}

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 New

func New() *Store

New builds an empty store.

func (*Store) Add

func (s *Store) Add(p Profile) (*Profile, error)

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) Delete

func (s *Store) Delete(name string) error

Delete removes a profile by name.

func (*Store) DeleteByID added in v1.0.87

func (s *Store) DeleteByID(id string) (string, error)

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

func (s *Store) FailOpenScope(name string) (id, gen string, ok bool)

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

func (s *Store) FailOpenScopeByID(id string) (scope, gen string, resolved bool)

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

func (s *Store) GetByID(id string) *Profile

GetByID returns a copy of the profile with the given stable ID, or nil.

func (*Store) GetByName

func (s *Store) GetByName(name string) *Profile

GetByName returns a profile by name (case-insensitive). O(1). nil if not found.

func (*Store) List

func (s *Store) List() []Profile

List returns a copy of all profiles (safe for JSON serialization).

func (*Store) Load

func (s *Store) Load(path string) error

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

func (s *Store) MutateDurable(ifVersion *int64, fn func() error) error

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) Names

func (s *Store) Names() []string

Names returns all profile names (for UI dropdowns), in insertion order.

func (*Store) Path

func (s *Store) Path() string

Path reports the persistence path ("" = disabled).

func (*Store) Rename added in v1.0.95

func (s *Store) Rename(id, newName string) (oldName string, err error)

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

func (s *Store) ReplaceAll(profiles []Profile)

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

func (s *Store) SaveErr() error

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

func (s *Store) SetPathForTest(path string)

SetPathForTest points persistence at path without loading.

func (*Store) SnapshotView added in v1.0.218

func (s *Store) SnapshotView() Snapshot

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

func (s *Store) Update(p Profile) error

Update replaces an existing profile's fields (by name). Preserves ID + CreatedAt.

func (*Store) UpdateByID added in v1.0.87

func (s *Store) UpdateByID(id string, p Profile) error

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.

func (*Store) Version added in v1.0.218

func (s *Store) Version() int64

Version returns the durable per-store mutation generation (the ifVersion fence value clients echo back on mutations).

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

Jump to

Keyboard shortcuts

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