From b991baaf72ed15e5b8ab6e5243c6f8a7375d33e5 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 24 May 2026 17:51:16 +0800 Subject: [PATCH] fix(io,tunnel): nil deref in RecorderObjectFromContext, CloseRead checked Writer, SetReadDeadline no unwrap - entrypoint.go: guard ictx.RecorderObjectFromContext with nil checks on ClientID and Redirect assignments - io.go: fix CloseRead checking Writer instead of Reader, add Reader fallback to CloseWrite, unwrap *readWriter/*readWriteCloser in SetReadDeadline to reach underlying net.Conn - docs: add package doc and exported symbol comments to internal/ctx and internal/io --- handler/tunnel/entrypoint.go | 14 ++++++--- internal/ctx/value.go | 12 ++++++++ internal/io/io.go | 55 ++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/handler/tunnel/entrypoint.go b/handler/tunnel/entrypoint.go index 0ec16549..abd2f7d6 100644 --- a/handler/tunnel/entrypoint.go +++ b/handler/tunnel/entrypoint.go @@ -147,8 +147,9 @@ func (ep *entrypoint) dial(ctx context.Context, network, addr string) (conn net. return nil, fmt.Errorf("%w %s", ErrTunnelRoute, addr) } - ro := ictx.RecorderObjectFromContext(ctx) - ro.ClientID = tunnelID.String() + if ro := ictx.RecorderObjectFromContext(ctx); ro != nil { + ro.ClientID = tunnelID.String() + } if tunnelID.IsPrivate() { return nil, fmt.Errorf("%w: tunnel %s is private for host %s", ErrPrivateTunnel, tunnelID, addr) @@ -172,8 +173,11 @@ func (ep *entrypoint) dial(ctx context.Context, network, addr string) (conn net. } log.Debugf("dial: connected to host %s, tunnel: %s, connector: %s", addr, tunnelID, cid) + ro := ictx.RecorderObjectFromContext(ctx) if node == ep.node { - ro.Redirect = "" + if ro != nil { + ro.Redirect = "" + } var clientAddr string if addr := xctx.SrcAddrFromContext(ctx); addr != nil { @@ -197,7 +201,9 @@ func (ep *entrypoint) dial(ctx context.Context, network, addr string) (conn net. return nil, err } } else { - ro.Redirect = node + if ro != nil { + ro.Redirect = node + } } return conn, nil diff --git a/internal/ctx/value.go b/internal/ctx/value.go index cdab85da..b5a7b3b4 100644 --- a/internal/ctx/value.go +++ b/internal/ctx/value.go @@ -1,3 +1,5 @@ +// Package ctx provides typed context value helpers for passing per-request +// data (buffer, logger, metadata, recorder objects) through the handler chain. package ctx import ( @@ -11,10 +13,14 @@ import ( type bufferKey struct{} +// ContextWithBuffer returns a copy of ctx with the given buffer stored. +// The buffer is used by dialers and connectors to reuse a *bytes.Buffer +// for protocol handshake data (e.g. TLS ClientHello, HTTP requests). func ContextWithBuffer(ctx context.Context, buffer *bytes.Buffer) context.Context { return context.WithValue(ctx, bufferKey{}, buffer) } +// BufferFromContext returns the *bytes.Buffer stored in ctx, or nil. func BufferFromContext(ctx context.Context) *bytes.Buffer { v, _ := ctx.Value(bufferKey{}).(*bytes.Buffer) return v @@ -22,10 +28,12 @@ func BufferFromContext(ctx context.Context) *bytes.Buffer { type loggerKey struct{} +// ContextWithLogger returns a copy of ctx with the given logger stored. func ContextWithLogger(ctx context.Context, log logger.Logger) context.Context { return context.WithValue(ctx, loggerKey{}, log) } +// LoggerFromContext returns the logger stored in ctx, or nil. func LoggerFromContext(ctx context.Context) logger.Logger { v, _ := ctx.Value(loggerKey{}).(logger.Logger) return v @@ -33,10 +41,12 @@ func LoggerFromContext(ctx context.Context) logger.Logger { type mdKey struct{} +// ContextWithMetadata returns a copy of ctx with the given metadata stored. func ContextWithMetadata(ctx context.Context, md metadata.Metadata) context.Context { return context.WithValue(ctx, mdKey{}, md) } +// MetadataFromContext returns the metadata stored in ctx, or nil. func MetadataFromContext(ctx context.Context) metadata.Metadata { v, _ := ctx.Value(mdKey{}).(metadata.Metadata) return v @@ -44,10 +54,12 @@ func MetadataFromContext(ctx context.Context) metadata.Metadata { type recorderObjectCtxKey struct{} +// ContextWithRecorderObject returns a copy of ctx with the given recorder object stored. func ContextWithRecorderObject(ctx context.Context, ro *xrecorder.HandlerRecorderObject) context.Context { return context.WithValue(ctx, recorderObjectCtxKey{}, ro) } +// RecorderObjectFromContext returns the recorder object stored in ctx, or nil. func RecorderObjectFromContext(ctx context.Context) *xrecorder.HandlerRecorderObject { v, _ := ctx.Value(recorderObjectCtxKey{}).(*xrecorder.HandlerRecorderObject) return v diff --git a/internal/io/io.go b/internal/io/io.go index a2208e4c..f51bc3fc 100644 --- a/internal/io/io.go +++ b/internal/io/io.go @@ -1,3 +1,6 @@ +// Package io provides composite IO types that combine separate readers and +// writers into standard [io.ReadWriter] and [io.ReadWriteCloser] pairs, plus +// helpers for half-close and read-deadline propagation. package io import ( @@ -7,13 +10,25 @@ import ( ) var ( + // ErrUnsupported is returned when a half-close operation is not supported + // by the underlying reader or writer. ErrUnsupported = errors.New("unsupported") ) +// CloseRead is the interface that groups the basic CloseRead method. +// +// CloseRead closes the read side of a connection. After CloseRead, reads +// return io.EOF while writes remain unaffected. It is typically implemented +// by [net.TCPConn] and connection wrappers throughout the x module. type CloseRead interface { CloseRead() error } +// CloseWrite is the interface that groups the basic CloseWrite method. +// +// CloseWrite closes the write side of a connection. After CloseWrite, writes +// return an error while reads continue to work. It is typically implemented +// by [net.TCPConn] and connection wrappers throughout the x module. type CloseWrite interface { CloseWrite() error } @@ -23,6 +38,9 @@ type readWriter struct { io.Writer } +// NewReadWriter returns an [io.ReadWriter] that reads from r and writes to w. +// The returned value supports [CloseRead] and [CloseWrite] by delegating to +// the corresponding methods on r or w when available. func NewReadWriter(r io.Reader, w io.Writer) io.ReadWriter { return &readWriter{ Reader: r, @@ -31,6 +49,9 @@ func NewReadWriter(r io.Reader, w io.Writer) io.ReadWriter { } func (rw *readWriter) CloseRead() error { + if sc, ok := rw.Reader.(CloseRead); ok { + return sc.CloseRead() + } if sc, ok := rw.Writer.(CloseRead); ok { return sc.CloseRead() } @@ -41,6 +62,9 @@ func (rw *readWriter) CloseWrite() error { if sc, ok := rw.Writer.(CloseWrite); ok { return sc.CloseWrite() } + if sc, ok := rw.Reader.(CloseWrite); ok { + return sc.CloseWrite() + } return ErrUnsupported } @@ -50,6 +74,10 @@ type readWriteCloser struct { io.Closer } +// NewReadWriteCloser returns an [io.ReadWriteCloser] that reads from r, writes +// to w, and closes c. Close delegates to c. The returned value also supports +// [CloseRead] and [CloseWrite] by delegating to the corresponding methods on +// r or w when available. func NewReadWriteCloser(r io.Reader, w io.Writer, c io.Closer) io.ReadWriteCloser { return &readWriteCloser{ Reader: r, @@ -59,6 +87,9 @@ func NewReadWriteCloser(r io.Reader, w io.Writer, c io.Closer) io.ReadWriteClose } func (rwc *readWriteCloser) CloseRead() error { + if sc, ok := rwc.Reader.(CloseRead); ok { + return sc.CloseRead() + } if sc, ok := rwc.Writer.(CloseRead); ok { return sc.CloseRead() } @@ -69,6 +100,9 @@ func (rwc *readWriteCloser) CloseWrite() error { if sc, ok := rwc.Writer.(CloseWrite); ok { return sc.CloseWrite() } + if sc, ok := rwc.Reader.(CloseWrite); ok { + return sc.CloseWrite() + } return ErrUnsupported } @@ -76,9 +110,30 @@ type setReadDeadline interface { SetReadDeadline(t time.Time) error } +// SetReadDeadline sets the read deadline on rw if the underlying connection +// supports it. It checks rw itself first, then unwraps *readWriter and +// *readWriteCloser to reach their embedded Reader or Writer. If no +// settable deadline is found the call is a no-op and returns nil. func SetReadDeadline(rw io.ReadWriter, t time.Time) error { if v, _ := rw.(setReadDeadline); v != nil { return v.SetReadDeadline(t) } + // Unwrap readWriter/readWriteCloser to reach the underlying connection. + switch r := rw.(type) { + case *readWriter: + if v, _ := r.Reader.(setReadDeadline); v != nil { + return v.SetReadDeadline(t) + } + if v, _ := r.Writer.(setReadDeadline); v != nil { + return v.SetReadDeadline(t) + } + case *readWriteCloser: + if v, _ := r.Reader.(setReadDeadline); v != nil { + return v.SetReadDeadline(t) + } + if v, _ := r.Writer.(setReadDeadline); v != nil { + return v.SetReadDeadline(t) + } + } return nil }