k8s-operator/sessionrecording/ws: unify Read/Write frame parsing (#19227)

Consolidate the duplicated WebSocket frame-parsing logic from Read
and Write into a shared processFrames loop, fixing several bugs in
the process:

- Mixed control and data frames in a single Read/Write call buffer
  were not handled: a control frame would cause merged data frames
  to be skipped.
- Multiple data frames into one Write call weren't being correctly
  parsed: only the first frame was processed, ignoring the rest in
  the buffer.
- msg.isFinalized was being set before confirming the fragment was
  complete, so an incomplete msg fragment, could've been sometimes
  marked as finalized.
- Continuation frames without any payload were being treated as if
  they didn't have stream ID, even thought the id is already known
  from the initial fragment.

Fixes tailscale/corp#39583

Signed-off-by: Fernando Serboncini <fserb@tailscale.com>
Signed-off-by: chaosinthecrd <tom@tmlabs.co.uk>
Co-authored-by: chaosinthecrd <tom@tmlabs.co.uk>
This commit is contained in:
Fernando Serboncini
2026-04-07 15:59:10 -04:00
committed by GitHub
parent 8a7e160a6e
commit 07399275f1
3 changed files with 217 additions and 168 deletions
+10 -3
View File
@@ -99,19 +99,19 @@ func (msg *message) Parse(b []byte, log *zap.SugaredLogger) (bool, error) {
}
isInitialFragment := len(msg.raw) == 0
msg.isFinalized = isFinalFragment(b)
finalized := isFinalFragment(b)
maskSet := isMasked(b)
payloadLength, payloadOffset, maskOffset, err := fragmentDimensions(b, maskSet)
if err != nil {
return false, fmt.Errorf("error determining payload length: %w", err)
}
log.Debugf("parse: parsing a message fragment with payload length: %d payload offset: %d maskOffset: %d mask set: %t, is finalized: %t, is initial fragment: %t", payloadLength, payloadOffset, maskOffset, maskSet, msg.isFinalized, isInitialFragment)
log.Debugf("parse: parsing a message fragment with payload length: %d payload offset: %d maskOffset: %d mask set: %t, is finalized: %t, is initial fragment: %t", payloadLength, payloadOffset, maskOffset, maskSet, finalized, isInitialFragment)
if len(b) < int(payloadOffset+payloadLength) { // incomplete fragment
return false, nil
}
msg.isFinalized = finalized
// TODO (irbekrm): perhaps only do this extra allocation if we know we
// will need to unmask?
msg.raw = make([]byte, int(payloadOffset)+int(payloadLength))
@@ -136,6 +136,13 @@ func (msg *message) Parse(b []byte, log *zap.SugaredLogger) (bool, error) {
// message payload.
// https://github.com/kubernetes/apimachinery/commit/73d12d09c5be8703587b5127416eb83dc3b7e182#diff-291f96e8632d04d2d20f5fb00f6b323492670570d65434e8eac90c7a442d13bdR23-R36
if len(msgPayload) == 0 {
if !isInitialFragment {
// Continuation frame with zero payload. The stream ID is
// already known from the initial fragment, so this is not
// fatal, just unusual.
log.Infof("[unexpected] received a continuation fragment with no payload")
return true, nil
}
return false, errors.New("[unexpected] received a message fragment with no stream ID")
}